53

我想我需要创建一个新的 SSL 套接字工厂?另外,出于显而易见的原因,我不想使用全局 SSL 上下文 ( https://github.com/square/okhttp/issues/184 )。

谢谢!

编辑:

从 okhttp 2.1.0 开始,您可以非常轻松地固定证书。

请参阅此处的源代码以开始使用

4

5 回答 5

89

OKHTTP 3.0 的更新

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

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

过去的一切都适用于 OKHTTP 的旧 (2.x) 版本

阅读这篇博文后,我能够修改用于 OkHttp 的概念。如果要避免使用全局 SSL 上下文,则应至少使用 2.0 版。

此修改仅适用于 OkHttp 的当前实例,并更改该实例以使其接受来自指定证书的证书。如果您希望接受其他证书(例如来自 Twitter 的证书),您只需创建一个新的 OkHttp 实例,无需进行下文所述的修改。

1. 创建信任库

为了固定证书,您首先需要创建一个包含此证书的信任库。为了创建信任库,我们将使用 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..."

要运行此脚本,您需要 3 件事:

  1. 确保keytool(包含在 Android SDK 中)在您的 $PATH 上。
  2. 确保您在与脚本相同的目录中下载了最新的 BouncyCastle jar 文件。(在这里下载)
  3. 您要固定的证书。

现在运行脚本

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

键入“yes”以信任证书,完成mytruststore.bks后将在您当前的目录中生成。

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

raw在您的文件夹下创建一个目录res。复制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传入 a 。SSLSocketFactory确保设置TRUST_STORE_PASSWORD为传递给 shell 脚本的密码。您的 OkHttp 实例现在应该只接受您指定的证书。

于 2014-06-03T05:10:39.030 回答
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:证书固定失败!
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 外部 CA 根

publicobject.com 的固定证书:

sha1/BOGUSPIN
在 com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
在 com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
在 com.squareup.okhttp.Connection.connect(Connection.java)
在 com .squareup.okhttp.Connection.connectAndSetOwner(Connection.java)

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

旁注:如果您在 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());

这里的所有都是它的!

于 2015-08-18T21:34:04.713 回答
16

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

openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
于 2017-07-12T10:58:33.950 回答
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”作为精确的站点证书......或者您可以根据您所需的安全状态为 CA Root 使用最广泛的哈希“T5x9IXmcrQ7YuQxXnxoCmeeQ84c”。

于 2015-05-11T03:01:49.077 回答
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);
于 2015-10-13T14:51:11.463 回答