使用自签名证书接受HTTPS连接

171
我正在尝试使用 HttpClient 库建立 HTTPS 连接,但是问题在于证书没有由诸如 VerisignGlobalSIgn 等在 Android 可信证书集中列出的被认可的证书颁发机构(CA)签名,因此我一直收到 javax.net.ssl.SSLException: Not trusted server certificate 的错误提示。
我看到了一些简单接受所有证书的解决方案,但如果我想要询问用户怎么办呢?
我希望获得一个类似于浏览器的对话框,让用户决定是否继续。最好能够使用与浏览器相同的证书存储。有什么想法吗?

这个已被接受的解决方案对我有用- https://dev59.com/b3E85IYBdhLWcg3wtV_1?noredirect=1&lq=1 - Venkatesh
13个回答

174

首先,您需要设置验证级别。这些级别不多:

  • ALLOW_ALL_HOSTNAME_VERIFIER
  • BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
  • STRICT_HOSTNAME_VERIFIER

虽然在新版Apache库中方法setHostnameVerifier()已经过时,但是对于Android SDK中的版本来说它很正常。 因此我们选择ALLOW_ALL_HOSTNAME_VERIFIER并将其设置在工厂方法SSLSocketFactory.setHostnameVerifier()中。

接下来,您需要为协议设置工厂为https。只需调用SchemeRegistry.register()方法即可完成此操作。

然后,您需要使用SingleClientConnManager创建DefaultHttpClient。 在下面的代码中,您还可以看到默认情况下也会使用我们的标志(ALLOW_ALL_HOSTNAME_VERIFIER),通过方法HttpsURLConnection.setDefaultHostnameVerifier()实现。

以下代码适用于我:

HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;

DefaultHttpClient client = new DefaultHttpClient();

SchemeRegistry registry = new SchemeRegistry();
SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier);
registry.register(new Scheme("https", socketFactory, 443));
SingleClientConnManager mgr = new SingleClientConnManager(client.getParams(), registry);
DefaultHttpClient httpClient = new DefaultHttpClient(mgr, client.getParams());

// Set verifier     
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);

// Example send http request
final String url = "https://encrypted.google.com/";
HttpPost httpPost = new HttpPost(url);
HttpResponse response = httpClient.execute(httpPost);

6
很遗憾,我无法使这段代码工作,我仍然收到“未受信任的服务器证书”的提示。是否有任何额外权限需要设置才能使它正常工作? - Juriy
1
这段代码不是接受所有证书吗?我需要一个弹窗来接受它。 - Morten
3
我正在使用org.apache.http.conn.ssl.SSLSocketFactory,为什么要使用javax.net.ssl.HttpsURLConnection - Someone Somewhere
10
你能解释一下这段代码相比完全禁用证书验证有什么好处吗?我不太熟悉安卓的SSL API,但乍一看,这似乎对主动攻击者毫无保障。 - CodesInChaos
3
建议使用ThreadSafeClientConnManager而不是SingleClientConnManager。 - Farm
显示剩余18条评论

130
以下是从证书颁发机构获得安全连接所需的主要步骤,这些机构在android平台上不被视为受信任的。
按照许多用户的要求,我在此处镜像了我博客文章中最重要的部分:
  1. 获取所有必需的证书(根证书和任何中间CA)
  2. 使用keytool和BouncyCastle提供程序创建密钥库并导入证书
  3. 在Android应用程序中加载keystore并将其用于安全连接(我建议使用Apache HttpClient而不是标准的java.net.ssl.HttpsURLConnection (易于理解,性能更高)

获取证书

您必须获取从终端证书到根CA构建链的所有证书。 这意味着,任何(如果存在)中间CA证书以及根CA证书。 您不需要获取终端证书。

创建密钥库

下载BouncyCastle Provider并将其存储到已知位置。 还要确保您可以调用keytool命令(通常位于JRE安装的bin文件夹下)。

现在将获得的证书(不导入终端证书)导入BouncyCastle格式的keystore中。

我没有测试过,但我认为导入证书的顺序很重要。 这意味着,首先导入最低的中间CA证书,然后一路上导入根CA证书。

使用以下命令可以创建一个新的keystore(如果尚未存在),密码为mysecret,并导入中间CA证书。 我还定义了BouncyCastle提供程序,在我的文件系统中找到它以及keystore格式。 为链中的每个证书执行此命令。

keytool -importcert -v -trustcacerts -file "path_to_cert/interm_ca.cer" -alias IntermediateCA -keystore "res/raw/mykeystore.bks" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "path_to_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass mysecret

验证证书是否已正确导入密钥库:

keytool -list -keystore "res/raw/mykeystore.bks" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "path_to_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass mysecret

应输出整个链:

RootCA, 22.10.2010, trustedCertEntry, Thumbprint (MD5): 24:77:D9:A8:91:D1:3B:FA:88:2D:C2:FF:F8:CD:33:93
IntermediateCA, 22.10.2010, trustedCertEntry, Thumbprint (MD5): 98:0F:C3:F8:39:F7:D8:05:07:02:0D:E3:14:5B:29:43

现在,您可以将密钥库作为原始资源复制到Android应用程序中的res/raw/目录下。

在应用程序中使用密钥库

首先,我们必须创建一个自定义的Apache HttpClient,用于HTTPS连接并使用我们的密钥库:

import org.apache.http.*

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.mykeystore);
            try {
                // Initialize the keystore with the provided trusted certificates
                // Also provide the password of the keystore
                trusted.load(in, "mysecret".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);
        }
    }
}
我们已经创建了自定义的HttpClient,现在我们可以使用它进行安全连接。例如,当我们对REST资源进行GET调用时:
// Instantiate the custom HttpClient
DefaultHttpClient client = new MyHttpClient(getApplicationContext());
HttpGet get = new HttpGet("https://www.mydomain.ch/rest/contacts/23");
// Execute the GET call and obtain the response
HttpResponse getResponse = client.execute(get);
HttpEntity responseEntity = getResponse.getEntity();

就是这样了;)


9
这只有在发布应用程序之前获取证书时才有用。它并不能帮助用户接受其自己的应用程序证书。 - Fuzzy
大家好,有人能告诉我如何验证上述实现中的密钥库和信任库吗?先谢谢了。 - Quick learner
这个之前运行得很好,但是现在我重新给服务器的证书换了一个密钥后出现了问题。每次更新服务器上的证书时,客户端存储器也需要更新,这似乎有些奇怪。一定有更好的方法 :| - bpn
很好的答案,我建议使用ThreadSafeClientConnManager而不是SingleClientConnManager。 - Farm
我已经添加了/res/raw/mykeystore.bks,但无法解决对它的引用。如何解决这个问题? - uniruddh
显示剩余3条评论

21
如果服务器上有自定义或自签名证书,而设备上没有该证书,则可以使用以下类在Android客户端加载并使用它:

将证书*.crt文件放置在/res/raw中,以便从R.raw.*可用。

使用以下类来获取具有使用该证书的套接字工厂的HTTPClientHttpsURLConnection

package com.example.customssl;

import android.content.Context;
import org.apache.http.client.HttpClient;
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.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

public class CustomCAHttpsProvider {

    /**
     * Creates a {@link org.apache.http.client.HttpClient} which is configured to work with a custom authority
     * certificate.
     *
     * @param context       Application Context
     * @param certRawResId  R.raw.id of certificate file (*.crt). Should be stored in /res/raw.
     * @param allowAllHosts If true then client will not check server against host names of certificate.
     * @return Http Client.
     * @throws Exception If there is an error initializing the client.
     */
    public static HttpClient getHttpClient(Context context, int certRawResId, boolean allowAllHosts) throws Exception {


        // build key store with ca certificate
        KeyStore keyStore = buildKeyStore(context, certRawResId);

        // init ssl socket factory with key store
        SSLSocketFactory sslSocketFactory = new SSLSocketFactory(keyStore);

        // skip hostname security check if specified
        if (allowAllHosts) {
            sslSocketFactory.setHostnameVerifier(new AllowAllHostnameVerifier());
        }

        // basic http params for client
        HttpParams params = new BasicHttpParams();

        // normal scheme registry with our ssl socket factory for "https"
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));

        // create connection manager
        ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);

        // create http client
        return new DefaultHttpClient(cm, params);
    }

    /**
     * Creates a {@link javax.net.ssl.HttpsURLConnection} which is configured to work with a custom authority
     * certificate.
     *
     * @param urlString     remote url string.
     * @param context       Application Context
     * @param certRawResId  R.raw.id of certificate file (*.crt). Should be stored in /res/raw.
     * @param allowAllHosts If true then client will not check server against host names of certificate.
     * @return Http url connection.
     * @throws Exception If there is an error initializing the connection.
     */
    public static HttpsURLConnection getHttpsUrlConnection(String urlString, Context context, int certRawResId,
                                                           boolean allowAllHosts) throws Exception {

        // build key store with ca certificate
        KeyStore keyStore = buildKeyStore(context, certRawResId);

        // 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 sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);

        // Create a connection from url
        URL url = new URL(urlString);
        HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
        urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

        // skip hostname security check if specified
        if (allowAllHosts) {
            urlConnection.setHostnameVerifier(new AllowAllHostnameVerifier());
        }

        return urlConnection;
    }

    private static KeyStore buildKeyStore(Context context, int certRawResId) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        // init a default key store
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);

        // read and add certificate authority
        Certificate cert = readCert(context, certRawResId);
        keyStore.setCertificateEntry("ca", cert);

        return keyStore;
    }

    private static Certificate readCert(Context context, int certResourceId) throws CertificateException, IOException {

        // read certificate resource
        InputStream caInput = context.getResources().openRawResource(certResourceId);

        Certificate ca;
        try {
            // generate a certificate
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            ca = cf.generateCertificate(caInput);
        } finally {
            caInput.close();
        }

        return ca;
    }

}

要点:

  1. Certificate 对象从 .crt 文件生成。
  2. 默认的 KeyStore 被创建。
  3. keyStore.setCertificateEntry("ca", cert) 将证书添加到别名为“ca”的密钥库中。您可以修改代码以添加更多证书(如中间 CA)。
  4. 主要目标是生成一个可以由 HTTPClientHttpsURLConnection 使用的 SSLSocketFactory
  5. SSLSocketFactory 还可以进一步配置,例如跳过主机名验证等。

更多信息请参见:http://developer.android.com/training/articles/security-ssl.html


我在哪里可以获取.crt文件?从服务器下载吗? - zionpi
@zionpi 证书文件将与您连接的启用了TLS的服务器使用相同。 - S.D.
谢谢!这太容易了! - kapil thadani
@S.D. 我该如何使用 .P12 文件代替 .crt 文件? - Rakesh R Nair
我有一个类似的疑问,你能帮忙吗?https://stackoverflow.com/questions/57389622/client-certificate-based-multifactor-authentication-in-android# - StezPet

16

我曾经尝试使用https连接我的Android App和RESTful服务,并感到很沮丧。此外,所有建议完全禁用证书检查的答案都让我有点烦恼。如果这样做,那么https还有什么意义呢?

在谷歌上搜索了一段时间后,我终于找到了这个解决方案,它不需要外部JAR包,只需使用Android API即可。感谢安德鲁·史密斯(Andrew Smith)在2014年7月发布了这篇文章。

 /**
 * Set up a connection to myservice.domain using HTTPS. An entire function
 * is needed to do this because myservice.domain has a self-signed certificate.
 * 
 * The caller of the function would do something like:
 * HttpsURLConnection urlConnection = setUpHttpsConnection("https://littlesvr.ca");
 * InputStream in = urlConnection.getInputStream();
 * And read from that "in" as usual in Java
 * 
 * Based on code from:
 * https://developer.android.com/training/articles/security-ssl.html#SelfSigned
 */
public static HttpsURLConnection setUpHttpsConnection(String urlString)
{
    try
    {
        // Load CAs from an InputStream
        // (could be from a resource or ByteArrayInputStream or ...)
        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        // My CRT file that I put in the assets folder
        // I got this file by following these steps:
        // * Go to https://littlesvr.ca using Firefox
        // * Click the padlock/More/Security/View Certificate/Details/Export
        // * Saved the file as littlesvr.crt (type X.509 Certificate (PEM))
        // The MainActivity.context is declared as:
        // public static Context context;
        // And initialized in MainActivity.onCreate() as:
        // MainActivity.context = getApplicationContext();
        InputStream caInput = new BufferedInputStream(MainActivity.context.getAssets().open("littlesvr.crt"));
        Certificate ca = cf.generateCertificate(caInput);
        System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());

        // Create a KeyStore containing our trusted CAs
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, 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);

        // Tell the URLConnection to use a SocketFactory from our SSLContext
        URL url = new URL(urlString);
        HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
        urlConnection.setSSLSocketFactory(context.getSocketFactory());

        return urlConnection;
    }
    catch (Exception ex)
    {
        Log.e(TAG, "Failed to establish SSL connection to server: " + ex.toString());
        return null;
    }
}

它对我的模型应用程序非常有效。


1
X509Certificate 应该导入 java 还是 javax? - Siddharth
1
我导入了 import java.security.cert.X509Certificate; - Gonzalo Fernández
很棒的解决方案! - Marcin Bortel
这是绝对最佳的解决方案,适用于此问题和许多其他问题。谢谢! - Squareoot

8
Google推荐使用Android Volley进行HTTP/HTTPS连接, 因为HttpClient已经过时。所以,你知道正确的选择:)。
此外,绝不能摧毁SSL证书(绝对不可以!!!)
摧毁SSL证书完全违背了SSL的目的,即促进安全性。如果您计划轰炸所有SSL证书,那么使用SSL就没有任何意义。更好的解决方案是在您的应用程序上创建自定义TrustManager + 使用Android Volley进行HTTP/HTTPS连接。
这里有一个Gist,我创建了一个基本的LoginApp,在服务器端使用自签名证书执行HTTPS连接,并在应用程序中接受该证书。

这里还有另一个Gist,可能会有帮助,用于创建自签名 SSL 证书以在您的服务器上设置并在应用程序中使用该证书。 非常重要:您必须将由上述脚本生成的 .crt 文件复制到 Android 项目的 "raw" 目录中。


你好 Ivan,我从未使用过 SSL 证书。你能详细解释一下吗?我该如何获取 .crt 文件? - jlively
嗨,Jively!我明白了。是的,当然可以。但首先,你介意看一下我上面提到的第二个Gist吗?我在这个Gist上放了两个文件:一个是脚本使用的文件,另一个是脚本本身,它使用“openssl”二进制文件来读取文件,然后构建包含SSL证书(.crt)的文件。如果你能理解整个过程,请告诉我。问候:) - ivanleoncz
嗯,我看了那两个 Gist,但是我真的不太明白该怎么使用它们? - jlively

6

对我来说,最高票的答案不起作用。经过一些调查,我在“Android开发者”网站上找到了所需信息:https://developer.android.com/training/articles/security-ssl.html#SelfSigned

创建一个空的X509TrustManager实现即可解决问题:

private static class MyTrustManager implements X509TrustManager
{

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
         throws CertificateException
    {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
        throws CertificateException
    {
    }

    @Override
    public X509Certificate[] getAcceptedIssuers()
    {
        return null;
    }

}

...

HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
try
{
    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    TrustManager[] tmlist = {new MyTrustManager()};
    context.init(null, tmlist, null);
    conn.setSSLSocketFactory(context.getSocketFactory());
}
catch (NoSuchAlgorithmException e)
{
    throw new IOException(e);
} catch (KeyManagementException e)
{
    throw new IOException(e);
}
conn.setRequestMethod("GET");
int rcode = conn.getResponseCode();

请注意,这个TrustManager的空实现只是一个示例,如果在生产环境中使用它,会造成严重的安全威胁!

1
只是提供信息 - 我不知道当时是否是这样,但现在他们似乎强烈反对这种方法(请参见注释)https://developer.android.com/training/articles/security-ssl.html#UnknownCa - Saik Caskey

4
这是如何将其他证书添加到您的KeyStore以避免此问题的方法:在HTTPS上使用HttpClient信任所有证书。这不会像您要求的那样提示用户,但它会减少用户遇到“不受信任的服务器证书”错误的可能性。

仅供测试目的,您不能使用此技巧在Play商店中发布应用程序,因为它将被拒绝。 - ariel

4

创建 SSL 证书的最简单方法

打开 Firefox(我想使用 Chrome 也是可能的,但我用 FF 更容易)

访问您配有自签名 SSL 证书的开发网站。

点击证书(在站点名称旁边)

点击“更多信息”

点击“查看证书”

点击“详细信息”

点击“导出…”

选择“X.509 证书和链(PEM)”,选择要保存它的文件夹和名称,然后点击“保存”

进入命令行,转到下载 pem 文件的目录,然后执行“openssl x509 -inform PEM -outform DM -in .pem -out .crt”

将 .crt 文件复制到 Android 设备内 /sdcard 文件夹的根目录中

在 Android 设备上,转到“设置”>“安全”>“从存储设备安装”。

它应该检测到证书并允许您添加它到设备上。

浏览到您的开发网站。

第一次访问时,它应该要求您确认安全异常。就这些了。

该证书应该适用于您 Android 上安装的任何浏览器(如 Browser、Chrome、Opera、Dolphin 等等)

请记住,如果您从不同的域提供静态文件(我们都是页面速度控),您还需要为该域添加证书。


2

我写了一个小型库ssl-utils-android,用于在Android上信任特定的证书。

您只需通过给出来自资产目录的文件名即可轻松加载任何证书。

使用方法:

OkHttpClient client = new OkHttpClient();
SSLContext sslContext = SslUtils.getSslContextForCertificateFile(context, "BPClass2RootCA-sha2.cer");
client.setSslSocketFactory(sslContext.getSocketFactory());

2

对于我的开发平台,目标SDK 16,发布版本4.1.2,这些解决方法都没有起作用,所以我找到了一个解决方法。

我的应用程序使用“http://www.example.com/page.php?data=somedata”将数据存储在服务器上。

最近,page.php已经被移动到“https://www.secure-example.com/page.php”,并且我一直收到“javax.net.ssl.SSLException:未信任的服务器证书”。

与其接受单个页面的所有证书,从这个指南开始,我通过编写自己的page.php并将其发布在“http://www.example.com/page.php”上解决了我的问题。

<?php

caronte ("https://www.secure-example.com/page.php");

function caronte($url) {
    // build curl request
    $ch = curl_init();
    foreach ($_POST as $a => $b) {
        $post[htmlentities($a)]=htmlentities($b);
    }
    curl_setopt($ch, CURLOPT_URL,$url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($post));

    // receive server response ...
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $server_output = curl_exec ($ch);
    curl_close ($ch);

    echo $server_output;
}

?>

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