如何在Square OKHTTP中固定证书?

57

在这里检查最简单的解决方案:https://dev59.com/SF4b5IYBdhLWcg3wWwTQ#45855405 - Bajrang Hudda
5个回答

96

OKHTTP 3.0的更新

OKHTTP 3.0 具有证书锁定的内置支持(built-in support)。开始使用以下代码:

 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();

这将失败,因为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)

确保将这些添加到您的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();

以下所有内容仅适用于旧版(2.x)的OkHttp

在阅读此博客文章后,我能够修改其概念以便与OkHttp一起使用。如果您想避免使用全局SSL上下文,则应至少使用版本2.0。

此修改仅适用于当前OkHttp实例,并更改该实例,以使其接受从指定证书中指定的证书。如果您想要接受其他证书(例如来自Twitter的证书),只需创建一个没有下面所述修改的新的OkHttp实例即可。

1. 创建TrustStore

为了固定证书,您首先需要创建包含此证书的信任存储。为了创建信任存储,我们将使用nelenkov的这个方便脚本稍作修改:

#!/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..."

运行此脚本需要以下三个条件:

  1. 确保 keytool(包含在Android SDK中)在您的 $PATH 中。
  2. 确保您已经下载了最新的BouncyCastle jar文件,并保存在与脚本相同的目录中。(在此处下载here)。
  3. 您需要固定的证书。

现在运行脚本即可。

./gentruststore.sh your_cert.pem bcprov-jdk15on-150.jar your_secret_pass

输入“yes”以信任证书,完整后将在当前目录生成mytruststore.bks

2. 将您的TrustStore应用于Android项目

res文件夹下创建一个名为raw的目录。将mytruststore.bks复制到此处。

现在,这是一个非常简单的类,可以将您的证书固定到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);

        }
    }
}

正如您所看到的,我们实例化了一个新的 OkHttpClient 实例,并调用 setSslSocketFactory 方法,将具有自定义信任存储库的 SSLSocketFactory 传递给它。确保将 TRUST_STORE_PASSWORD 设置为您传递到 shell 脚本中的密码。现在,您的 OkHttp 实例应仅接受您指定的证书。


25
请勿为每个请求创建一个OkHttpClient。创建一个单一实例并在每个请求中重复使用它。 - Jake Wharton
4
这只是示例代码,当然你不需要为每个请求都这样做 - 请注意URL也是硬编码的。 - Martin Konecny
3
据我所知,证书不是应该保密的内容。在每次SSL握手时,您的服务器都会将其发送给客户端。在这种情况下需要密码,因为keytool通常用于存储私钥而不是证书。不幸的是,无法禁用密码,因此请使用任何您喜欢的密码。 - Martin Konecny
3
java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: 找不到证书路径的信任锚点。 - Aderbal Nunes
2
@SebastianRoth openssl x509 -inform der -in <certificate> -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64 如果这正是您所寻找的,那么输出的是您公钥的SHA1值,以Base64编码。 - Greg
显示剩余11条评论

27

使用OkHttp比我想象的要容易。

按照以下步骤进行:

1. 获取公共sha1密钥。 OkHttp文档提供了一种清晰的方法来完成此操作,并附带示例代码。如果该网站关闭,下面是复制的代码:

例如,要固定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();   

正如预期的那样,这会导致证书固定异常:

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

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)

通过将异常中的公钥哈希粘贴到证书固定配置中进行跟进:

附注:如果您在 Android 上执行此操作,并且是在 UI 线程上执行此操作,则会获得单独的异常,请确保您在后台线程上执行此操作。

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());

就是这样了!


+1 for the solution, 但是Retrofit没有使用我设置的带有CertificatePinner的OkHttpClient,有什么想法吗? - dhaval
1
@dhaval 你可以使用Retrofit Builder,并向其提供一个OkHttpClient。 - spierce7
@Bootstrapper 我并不是这方面的专家。但我刚刚经历了这个过程。我与我的IT团队合作,在测试环境中更换了证书,然后我成功地连接上了它。从我的测试中,我发现只要至少有一个证书匹配,请求仍将成功。不过你应该进行自己的测试,因为我只是简单地测试了一下,最终我们采取了不依赖于这种方法的措施。 - spierce7
2
我得到了这个异常,而不是带有sha256的异常,你有什么办法可以解决吗?javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. - Fonix
好的,看起来可能是服务器端的问题。当我使用我的UAT服务器时,它会出现那个错误,但是对于我的生产服务器来说,一切正常,所以必须是服务器上证书设置的方式有问题。不过我不确定具体是怎么回事,或许有人可以澄清一下。 - Fonix
显示剩余4条评论

17

如果您无法访问该域(例如受限制),并且无法测试伪哈希值,但您拥有证书文件,则可以使用openssl检索它:

openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

2
如果您的证书已经是DER格式(例如从Chrome中获取),则命令为openssl x509 -inform der -in certificate.cer -fingerprint -sha256 -noout | openssl dgst -sha256 -binary | openssl enc -base64 - Tom
请注意,并确保使用-pubkey(-fingerprint将为整个证书创建一个sha256,这是不正确的) - DummyData

6

为了详细说明@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

然后将所有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();

然而,根据我所进行的测试和代码审查,只有第一个有效的哈希值会被解释,因此最好只包括返回的一个哈希值。您可以使用最具体的哈希值“DmxUShsZuNiqPQsX2Oi9uv2sCnw”来获取精确的站点证书... 或者您可以使用最广泛的哈希值“T5x9IXmcrQ7YuQxXnxoCmeeQ84c”,以基于您期望的安全姿态的CA根。

1
你需要将这个发布到Square的Github上,但我认为作者的意图是在可能需要吊销并替换证书时提供备用哈希,以免对现有客户端代码造成破坏。当然,这只是我的想法。 - Michael Barany
@MichaelBarany,顺便说一下,iOS中的AFNetworking支持验证每个固定证书是否在链中以及验证至少有一个固定证书在链中。我不是哪种验证更理想的专家,但拥有这种灵活性很好。 - deRonbrown

3

我在developer.android.com/training/articles/security-ssl中的未知证书颁发机构部分找到了一个非常有用的例子。

可以在上下文中使用context.getSocketFactory()返回的SSLSocketFactory,然后在setSslSocketFactory()方法中将其设置为OkHttpClient。

注意:未知证书颁发机构部分还提到了下载证书文件以在此代码中使用和检查的链接。

这是我编写的获取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;
}

稍后我会像这样将其设置为OkHttpClient
httpClient.setSslSocketFactory(sslSocketFactory);

然后进行https调用

httpClient.newCall(requestBuilder.build()).enqueue(callback);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接