android - okhttpclient - ¿Cómo puedo anclar un certificado con Square OKHTTP?
okhttp vs retrofit (5)
ACTUALIZACIÓN PARA OKHTTP 3.0
OKHTTP 3.0 tiene soporte incorporado para fijar certificados. Comience pegando el siguiente código:
String hostname = "yourdomain.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
Request request = new Request.Builder()
.url("https://" + hostname)
.build();
client.newCall(request).execute();
Esto no funcionará porque AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
no es un hash válido de su certificado. La excepción lanzada tendrá los hash correctos de su certificado:
javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
Pinned certificates for publicobject.com:
sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
at okhttp3.CertificatePinner.check(CertificatePinner.java)
at okhttp3.Connection.upgradeToTls(Connection.java)
at okhttp3.Connection.connect(Connection.java)
at okhttp3.Connection.connectAndSetOwner(Connection.java)
Asegúrese de agregarlos a su objeto CertificatePinner y de haber anclado su certificado con éxito:
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
.add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
.add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
.build();
TODO EL PASADO AQUÍ ES PARA VERSIONES DE OKHTTP MÁS VIEJAS (2.x)
Después de leer esta publicación en el blog , pude modificar el concepto para utilizarlo con OkHttp. Debería usar al menos la versión 2.0 si quiere evitar el uso de un contexto SSL global.
Esta modificación se aplica solo a la instancia actual de OkHttp y cambia esa instancia para que solo acepte certificados del certificado especificado. Si desea que se acepten otros certificados (como uno de Twitter), simplemente necesita crear una nueva instancia de OkHttp sin las modificaciones que se describen a continuación.
1. Creando un TrustStore
Para fijar un certificado, primero necesita crear un almacén de confianza que contenga este certificado. Para crear el almacén de confianza utilizaremos este útil script de nelenkov ligeramente modificado para nuestros propósitos:
#!/bin/bash
if [ "$#" -ne 3 ]; then
echo "Usage: importcert.sh <CA cert PEM file> <bouncy castle jar> <keystore pass>"
exit 1
fi
CACERT=$1
BCJAR=$2
SECRET=$3
TRUSTSTORE=mytruststore.bks
ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT`
if [ -f $TRUSTSTORE ]; then
rm $TRUSTSTORE || exit 1
fi
echo "Adding certificate to $TRUSTSTORE..."
keytool -import -v -trustcacerts -alias $ALIAS /
-file $CACERT /
-keystore $TRUSTSTORE -storetype BKS /
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider /
-providerpath $BCJAR /
-storepass $SECRET
echo ""
echo "Added ''$CACERT'' with alias ''$ALIAS'' to $TRUSTSTORE..."
Para ejecutar este script necesitas 3 cosas:
- Asegúrese de que
keytool
(incluido en Android SDK) esté en su $ PATH. - Asegúrese de tener la última descarga del archivo jar BouncyCastle en el mismo directorio que el script. (Descargar here )
- El certificado que quieres anclar.
Ahora ejecuta el script
./gentruststore.sh your_cert.pem bcprov-jdk15on-150.jar your_secret_pass
Escriba ''yes'' para confiar en el certificado, y cuando complete mytruststore.bks
se generará en su directorio actual.
2. Aplique su TrustStore a su proyecto de Android
Crea un directorio sin raw
debajo de tu carpeta de res
. Copia mytruststore.bks
aquí.
Ahora aquí hay una clase muy simple que fija tu certificado en OkHttp
import android.content.Context;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.InputStream;
import java.io.Reader;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
/**
* Created by martin on 02/06/14.
*/
public class Pinning {
Context context;
public static String TRUST_STORE_PASSWORD = "your_secret";
private static final String ENDPOINT = "https://api.yourdomain.com/";
public Pinning(Context c) {
this.context = c;
}
private SSLSocketFactory getPinnedCertSslSocketFactory(Context context) {
try {
KeyStore trusted = KeyStore.getInstance("BKS");
InputStream in = context.getResources().openRawResource(R.raw.mytruststore);
trusted.load(in, TRUST_STORE_PASSWORD.toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trusted);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return sslContext.getSocketFactory();
} catch (Exception e) {
Log.e("MyApp", e.getMessage(), e);
}
return null;
}
public void makeRequest() {
try {
OkHttpClient client = new OkHttpClient();
client.setSslSocketFactory(getPinnedCertSslSocketFactory(context));
Request request = new Request.Builder()
.url(ENDPOINT)
.build();
Response response = client.newCall(request).execute();
Log.d("MyApp", response.body().string());
} catch (Exception e) {
Log.e("MyApp", e.getMessage(), e);
}
}
}
Como puede ver, instanciamos una nueva instancia de OkHttpClient
y llamamos a setSslSocketFactory
, pasando SSLSocketFactory
con nuestro almacén de confianza personalizado. Asegúrese de configurar TRUST_STORE_PASSWORD
a la contraseña que pasó al script de shell. Su instancia de OkHttp ahora solo debe aceptar el certificado que ha especificado.
Creo que necesito crear una nueva SSL Socket Factory? Además, no quiero utilizar el Contexto SSL global ( https://github.com/square/okhttp/issues/184 ) por razones obvias.
¡Gracias!
EDITAR:
A partir de okhttp 2.1.0 puede anclar certificados muy fácilmente.
Vea el código fuente aquí para comenzar
Encontré el ejemplo mencionado en la sección Autorización de certificados desconocidos de este enlace developer.android.com/training/articles/security-ssl muy útil.
El SSLSocketFactory devuelto en context.getSocketFactory () se puede usar para configurar OkHttpClient en el método setSslSocketFactory ().
Nota: La sección de la autoridad de certificación desconocida también menciona el enlace para descargar un archivo cert para usar y verificar este código.
Aquí está el método de muestra que he escrito para obtener SSLSocketFactory
private SSLSocketFactory getSslSocketFactory() {
try {
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = getApplicationContext().getResources().openRawResource(R.raw.loadder);
Certificate ca = null;
try {
ca = cf.generateCertificate(caInput);
System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} catch (CertificateException e) {
e.printStackTrace();
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
if (ca == null)
return null;
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
return context.getSocketFactory();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Más tarde solo estoy estableciendo esto en OkHttpClient como este
httpClient.setSslSocketFactory(sslSocketFactory);
y luego hacer la llamada https
httpClient.newCall(requestBuilder.build()).enqueue(callback);
Esto es más fácil de lo que pensaba con OkHttp.
Sigue estos pasos:
1. Obtenga las claves públicas sha1. La documentación de OkHttp nos brinda una manera clara de hacer esto completo con código de muestra. En caso de que desaparezca, aquí está pegado a continuación:
Por ejemplo, para fijar https://publicobject.com , comience con una configuración incorrecta:
String hostname = "publicobject.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha1/BOGUSPIN")
.build();
OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(certificatePinner);
Request request = new Request.Builder()
.url("https://" + hostname)
.build();
client.newCall(request).execute();
Como se esperaba, esto falla con una excepción de fijación de certificado:
javax.net.ssl.SSLPeerUnverifiedException: ¡fallo de fijación del certificado!
cadena de certificados Peer: sha1 / DmxUShsZuNiqPQsX2Oi9uv2sCnw =: CN = publicobject.com, OU = PositiveSSL sha1 / SXxoaOSEzPC6BgGmxAt / EAcsajw =: CN = COMODO RSA validación de dominios Secure CA servidor sha1 / blhOM3W9V / bVQhsWAcLYwPU6n24 =: CN = COMODO RSA Autoridad de Certificación sha1 / T5x9IXmcrQ7YuQxXnxoCmeeQ84c =: CN = AddTrust CA raíz externa
Certificados fijados para publicobject.com:
sha1 / BOGUSPIN
en com.squareup.okhttp.CertificatePinner.check (CertificatePinner.java)
en com.squareup.okhttp.Connection.upgradeToTls (Connection.java)
en com.squareup.okhttp.Connection.connect (Connection.java)
en com.squareup.okhttp.Connection.connectAndSetOwner (Connection.java)
Haga un seguimiento pegando los hash de las claves públicas de la excepción en la configuración del pinner del certificado:
Nota al pie: Si está haciendo esto en Android obtendrá una excepción por separado si está haciendo esto en un hilo de interfaz de usuario, así que asegúrese de hacerlo en un hilo de fondo.
2. Configure su OkHttp Client:
OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
.add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
.add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build());
¡Eso es todo al respecto!
Para ampliar el código fuente de muestra @ Michael-barany compartido, he realizado algunas pruebas y parece ser una muestra de código engañosa. En la muestra, el código de la excepción observó 4 hashes sha1 de la excepción de la cadena de certificados:
javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
luego, posteriormente, agregó los 4 valores hash de clave pública sha1 al CertificatePinner Builder.
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
.add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
.add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build();
Sin embargo, dadas las pruebas que realicé y revisé el código, solo se interpretaría el primero válido, por lo que sería más adecuado incluir solo UNO de los hashes devueltos. Puede usar el hash más específico "DmxUShsZuNiqPQsX2Oi9uv2sCnw" para el certificado de sitio preciso ... o puede usar el hash más amplio "T5x9IXmcrQ7YuQxXnxoCmeeQ84c" para la raíz de CA en función de la postura de seguridad que desee.
Si no tiene acceso al dominio (acceso restringido, por ejemplo) y no puede probar el hash falso, pero tiene un archivo de certificado, puede usar openssl para recuperarlo:
openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64