Android 如何使用 Square OKHTTP 固定证书?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/24006545/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How can I pin a certificate with Square OKHTTP?
提问by Michael Barany
I think I need to create a new SSL Socket Factory? Also, I don't want to use the global SSL Context (https://github.com/square/okhttp/issues/184) for obvious reasons.
我想我需要创建一个新的 SSL 套接字工厂?另外,出于显而易见的原因,我不想使用全局 SSL 上下文 ( https://github.com/square/okhttp/issues/184)。
thanks!
谢谢!
EDIT:
编辑:
As of okhttp 2.1.0 you can pin certificates very easily.
从 okhttp 2.1.0 开始,您可以非常轻松地固定证书。
See the source code hereto get started
回答by Martin Konecny
UPDATE FOR OKHTTP 3.0
OKHTTP 3.0 更新
OKHTTP 3.0 has built-in supportfor pinning certificates. Start off by pasting the following code:
OKHTTP 3.0内置了对固定证书的支持。首先粘贴以下代码:
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();
This will fail because AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
is not a valid hash of your certificate. The exception thrown will have the correct hashes of your certificate:
这将失败,因为AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
它不是您证书的有效哈希。抛出的异常将具有正确的证书哈希值:
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)
Make sure you add these to your CertificatePinner object, and you have successfully pinned your certificate:
确保将这些添加到您的 CertificatePinner 对象中,并且您已成功固定您的证书:
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();
EVERYTHING PAST HERE IS FOR OLDER (2.x) VERSIONS OF OKHTTP
过去的一切都是为了旧的 (2.x) 版本的 OKHTTP
After reading this blog postI was able to modify the concept for use with OkHttp. You should use at least version 2.0 if you want to avoid using a global SSL context.
阅读这篇博文后,我能够修改与 OkHttp 一起使用的概念。如果您想避免使用全局 SSL 上下文,您应该至少使用 2.0 版。
This modification applies only to the current instance of OkHttp, and changes that instance so that it onlyaccepts certificates from the certificate specified. If you want other certificates (such as one from Twitter) to be accepted, you simply need to create a new OkHttp instance without the modifications described below.
此修改仅适用于 OkHttp 的当前实例,并更改该实例以使其仅接受来自指定证书的证书。如果您希望其他证书(例如来自 Twitter 的证书)被接受,您只需创建一个新的 OkHttp 实例,无需进行下述修改。
1. Creating a TrustStore
1. 创建信任库
In order to pin a certificate, you first need to create a truststore containing this certificate. To create the truststore we will use this handy script from nelenkov slightly modified for our purposes:
为了固定证书,您首先需要创建一个包含此证书的信任库。为了创建信任库,我们将使用来自 nelenkov 的这个方便的脚本,为了我们的目的稍作修改:
#!/bin/bash
if [ "$#" -ne 3 ]; then
echo "Usage: importcert.sh <CA cert PEM file> <bouncy castle jar> <keystore pass>"
exit 1
fi
CACERT=
BCJAR=
SECRET=
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..."
To run this script you need 3 things:
要运行此脚本,您需要 3 件事:
- Make sure
keytool
(included in Android SDK) is on your $PATH. - Make sure you have the latest BouncyCastle jar file download in the same dir as the script. (Download here)
- The certificate you want to pin.
- 确保
keytool
(包含在 Android SDK 中)在您的 $PATH 上。 - 确保您在与脚本相同的目录中下载了最新的 BouncyCastle jar 文件。(在此下载)
- 要固定的证书。
Now run the script
现在运行脚本
./gentruststore.sh your_cert.pem bcprov-jdk15on-150.jar your_secret_pass
Type 'yes' to trust the certificate, and when complete mytruststore.bks
will be generated in your current dir.
键入“是”以信任证书,完成mytruststore.bks
后将在您当前的目录中生成。
2. Apply your TrustStore to your Android project
2. 将您的 TrustStore 应用到您的 Android 项目
Create a directory raw
under your res
folder. Copy mytruststore.bks
here.
raw
在您的res
文件夹下创建一个目录。复制mytruststore.bks
到这里。
Now here's a very simple class that pins your cert to OkHttp
现在这是一个非常简单的类,它将您的证书固定到 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);
}
}
}
As you can see we instantiate a new instance of OkHttpClient
and call setSslSocketFactory
, passing in a SSLSocketFactory
with our custom truststore. Make sure you set TRUST_STORE_PASSWORD
to the password you passed into the shell script. Your OkHttp instance should now only accept the certificate you specified.
如您所见,我们实例化了一个新实例OkHttpClient
并调用了setSslSocketFactory
,并传入了SSLSocketFactory
我们自定义的信任库。确保您设置TRUST_STORE_PASSWORD
为您传递给 shell 脚本的密码。您的 OkHttp 实例现在应该只接受您指定的证书。
回答by spierce7
This is easier than I thought with OkHttp.
使用 OkHttp 这比我想象的要容易。
Follow these steps:
按着这些次序:
1. Get the public sha1 keys.The OkHttp documentationgives us a clear way to do this complete with sample code. In case it goes away, here it is pasted in below:
1. 获取公共 sha1 密钥。该OkHttp文档给了我们一个明确的方式来做到这一点完整的示例代码。如果它消失了,请粘贴在下面:
For example, to pin https://publicobject.com, start with a broken configuration:
例如,要固定https://publicobject.com,请从损坏的配置开始:
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();
As expected, this fails with a certificate pinning exception:
正如预期的那样,这会因证书锁定异常而失败:
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
javax.net.ssl.SSLPeerUnverifiedException:证书固定失败!
同等证书链:SHA1 / DmxUShsZuNiqPQsX2Oi9uv2sCnw =:CN = publicobject.com,OU = PositiveSSL SHA1 / SXxoaOSEzPC6BgGmxAt / EAcsajw =:CN = COMODO RSA域验证安全服务器CA SHA1 / blhOM3W9V / bVQhsWAcLYwPU6n24 =:CN = COMODO RSA认证机构SHA1 / T5x9IXmcrQ7YuQxXnxoCmeeQ84c =: CN=AddTrust 外部 CA 根
Pinned certificates for publicobject.com:
publicobject.com 的固定证书:
sha1/BOGUSPIN
at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
at com.squareup.okhttp.Connection.connect(Connection.java)
at com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)
sha1/BOGUSPIN
at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
at com.squareup.okhttp.Connection.connect(Connection.java)
at com .squareup.okhttp.Connection.connectAndSetOwner(Connection.java)
Follow up by pasting the public key hashes from the exception into the certificate pinner's configuration:
通过将异常中的公钥哈希粘贴到证书 pinner 的配置中进行跟进:
Side note: If you are doing this on Android you will get a separate exception if you are doing this on a UI thread, so make sure you do this on a background thread.
旁注:如果您在 Android 上执行此操作,如果您在 UI 线程上执行此操作,则会得到一个单独的异常,因此请确保您在后台线程上执行此操作。
2. Configure your OkHttp Client:
2. 配置您的 OkHttp 客户端:
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());
That's all there is to it!
这里的所有都是它的!
回答by Grzegorz Pawe?czuk
If you don't have access to the domain (restricted access for example) and cant test bogus hash, but you have certificate file you can use openssl to retrieve it:
如果您无权访问域(例如限制访问)并且无法测试虚假哈希,但您有证书文件,则可以使用 openssl 来检索它:
openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
回答by Dave Cobb
To expand on the sample source code@Michael-barany shared, I have done some testing and it appears to be a misleading code sample. In the sample the code the exception noted 4 sha1 hashes from the certificate chain exception:
为了扩展@Michael-barany 共享的示例源代码,我做了一些测试,它似乎是一个误导性的代码示例。在示例代码中,异常记录了来自证书链异常的 4 个 sha1 哈希值:
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
then subsequently added all 4 sha1 public key hashes to the CertificatePinner Builder.
然后随后将所有 4 个 sha1 公钥哈希添加到 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();
However, given tests I have performed and reviewing the code, only the first valid one would be interpreted, so you would be best suited to only include ONE of the hashes returned. You could use the most specific hash "DmxUShsZuNiqPQsX2Oi9uv2sCnw" for the precise site certificate... or you could use the most broad hash "T5x9IXmcrQ7YuQxXnxoCmeeQ84c" for the CA Root based on your desired security posture.
但是,鉴于我已执行并查看代码的测试,只会解释第一个有效的,因此您最好只包含返回的一个哈希值。您可以使用最具体的哈希“DmxUShsZuNiqPQsX2Oi9uv2sCnw”作为精确的站点证书……或者您可以根据您所需的安全状况为 CA 根使用最广泛的哈希“T5x9IXmcrQ7YuQxXnxoCmeeQ84c”。
回答by Vinayak
I found the example mentioned in Unknown certificate authoritysection of this link developer.android.com/training/articles/security-sslvery useful.
我发现此链接developer.android.com/training/articles/security-ssl 的未知证书颁发机构部分中提到的示例非常有用。
The SSLSocketFactory returned in context.getSocketFactory() can be then used to set to OkHttpClient in setSslSocketFactory() method.
在 context.getSocketFactory() 中返回的 SSLSocketFactory 然后可用于在 setSslSocketFactory() 方法中设置为 OkHttpClient。
Note : The Unknown certificate authority section also mentions the link to download a cert file to use and check this code.
注意:未知证书颁发机构部分还提到了下载证书文件以使用和检查此代码的链接。
Here is the sample method I've written to obtain the SSLSocketFactory
这是我编写的用于获取 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;
}
Later I'm just setting this to OkHttpClient like this
后来我只是像这样将它设置为 OkHttpClient
httpClient.setSslSocketFactory(sslSocketFactory);
and then make the https call
然后进行 https 调用
httpClient.newCall(requestBuilder.build()).enqueue(callback);