我该如何让Android Volley执行HTTPS请求,使用由未知CA自签名的证书?

19

在提出问题之前,我找到了一些链接,逐个检查了它们,但没有一个给我提供解决方案:

迄今为止,我唯一找到的一个链接是这个,其中提供了两种方法:使用 Android Volley 进行 HTTPS 请求

  • 1º 指示将某些类导入到应用程序中,实际上有其他必须导入的类,并且这些类使用了“apache.org”的不推荐使用的库
  • 2º 一个 NUKE 所有 SSL 证书的示例(非常糟糕的想法...)


我还发现了这篇博客,其中有很多解释,但最终我意识到示例使用了来自“apache.org”的不推荐使用的库,而且该博客本身没有 Android Volley 的内容。 https://nelenkov.blogspot.mx/2011/12/using-custom-certificate-trust-store-on.html


还有来自 Android 的链接和“未知证书颁发机构”部分的代码,它提供了一个很好的解决方案,但是代码本身在结构上缺少一些东西(Android Studio 抱怨...):https://developer.android.com/training/articles/security-ssl.html

但是,来自该链接的以下引用似乎是解决问题的核心概念。

“TrustManager 是系统用于验证服务器证书的组件,并通过使用一个或多个 CA 创建的 KeyStore 创建 TrustManager,这些 CA 将是该 TrustManager 所信任的唯一 CA。 给定新的 TrustManager,示例初始化了一个新的 SSLContext,提供了一个 SSLSocketFactory,您可以使用它来覆盖 HttpsURLConnection 的默认 SSLSocketFactory。这样,连接将使用您的 CA 进行证书验证。”



现在,这是我的问题:我有一个正在使用自签名证书的 Web 服务器,并根据其证书创建了“BKS truststore”。我已将 BKS truststore 导入到我的 Android 应用程序中,现在,我在应用程序中有以下代码(我只是在此处发布了 MainActivity,这是目前涉及此主题的唯一类,我想):

package com.domain.myapp;


import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

import android.widget.EditText;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HurlStack;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.io.InputStream;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;


public class LoginScreen extends AppCompatActivity {

Context ctx          = null;
InputStream inStream = null;
HurlStack hurlStack  = null;

EditText username    = null;
EditText password    = null;
String loginStatus   = null;

public LoginScreen() {

    try {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        KeyStore ks = KeyStore.getInstance("BKS");
        inStream = ctx.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);
        ks.load(inStream, null);
        inStream.close();
        tmf.init(ks);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        hurlStack = new HurlStack(null, sslSocketFactory);
    } catch (Exception e){
        Log.d("Exception:",e.toString());
    }
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login_screen);
    username = (EditText) findViewById(R.id.user);
    password = (EditText) findViewById(R.id.passwd);
}

public void login(View view) {

    RequestQueue queue = Volley.newRequestQueue(this, hurlStack);
    final String url = "https://myserver.domain.com/app/login";

    StringRequest postRequest = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>()
            {
                @Override
                public void onResponse(String response) {
                    Log.d("Response", response);
                    loginStatus = "OK";
                }
            },
            new Response.ErrorListener()
            {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("Error.Response", String.valueOf(error));
                    loginStatus = "NOK";
                }
            }
    ) {
        @Override
        protected Map<String, String> getParams()
        {
            Map<String, String>  params = new HashMap<String, String>();
            params.put("username", String.valueOf(user));
            params.put("domain", String.valueOf(passwd));

            return params;
        }
    };
    queue.add(postRequest);

    if (loginStatus == "OK") {
        Intent intent = new Intent(LoginScreen.this, OptionScreen.class);
        startActivity(intent);
    } else {
        Toast.makeText(getApplicationContext(), "Login failed",Toast.LENGTH_SHORT).show();
    }
}

}

关于构造函数类,我已经复制了代码并添加了一些注释,说明我对每个部分的理解:

try {
// I have a TrustManagerFactory object
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// I have a KeyStore considering BKS (BOUNCY CASTLE) KeyStore object
KeyStore ks = KeyStore.getInstance("BKS");
// I have configured a inputStream using my TrustStore file as a Raw Resource
inStream = ctx.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);
// I have loaded my Raw Resource into the KeyStore object
ks.load(inStream, null);
inStream.close();
// I have initialiazed my Trust Manager Factory, using my Key Store Object
tmf.init(ks);
// I have created a new SSL Context object
SSLContext sslContext = SSLContext.getInstance("TLS");
// I have initialized my new SSL Context, with the configured Trust Managers found on my Trust Store
sslContext.init(null, tmf.getTrustManagers(), null);
// I have configured a HttpClientStack, using my brand new Socket Context
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
hurlStack = new HurlStack(null, sslSocketFactory);
} catch (Exception e){
Log.d("Exception:",e.toString());
}

接着,在另一个类方法中,我有一个请求队列(RequestQueue),使用的是我已经在类构造函数上配置好的HttpClientStack:

RequestQueue queue = Volley.newRequestQueue(this, hurlStack);
final String url = "https://myserver.domain.com/app/login";
StringRequest postRequest = new StringRequest(Request.Method.POST, url,new Response.Listener<String>()
    {
    ...
    ...
    }

当我在我的应用程序中输入Web服务器期望的用户名和密码并运行应用程序时,我可以在Android Studio的Android Monitor中看到以下消息:

09-17 21:57:13.842 20617-20617/com.domain.myapp D/Error.Response: com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

经过所有这些解释,我有以下问题:

  • 除了在类的构造函数中配置自定义TrustManager的CA之外,还需要配置什么才能使Android接受SSL证书?

很抱歉,我是一个初学者,不论是在Android编程上还是在Java上,所以也许我犯了一个可怕的错误...

任何帮助都将不胜感激。

更新

我已经改进了类的构造函数,更好地分组语句,并使用了KeyManagerFactory,这似乎在这个过程中非常重要。以下是代码:

public class LoginScreen extends AppCompatActivity {

...
...

  public LoginScreen() {

    try {
        inStream = this.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);

        KeyStore ks = KeyStore.getInstance("BKS");
        ks.load(inStream, "bks*password".toCharArray());
        inStream.close();

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
        kmf.init(ks, "bks*password".toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
        tmf.init(ks);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(), null);
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        hurlStack = new HurlStack(null, sslSocketFactory);
    } catch (Exception e){
        Log.d("Exception:",e.toString());
    }

  }

...
...

}

无论如何,我仍然遇到问题...

响应:com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: 未找到用于认证路径的信任锚点。


如果不需要客户端身份验证,就不需要KeyManagerFactory。您可以省略它。 "找不到认证路径的信任锚点"意味着SSL客户端在信任库中找不到服务器证书。您确定BKS包含服务器证书吗?您是否导入了根CA或最终证书?如果您已经导入了根CA,可能是服务器端的配置错误。尝试导入叶子证书。 - pedrofb
这可能会有所帮助:https://stackoverflow.com/a/66867513/191761 - Adam Burley
3个回答

8
我已经通过以下代码在我的Volley类中创建了新的requestQueue来实现https:

我通过以下代码在我的Volley类中创建了新的requestQueue来实现https:

public RequestQueue getRequestQueue() {
    if (mRequestQueue == null) {
        mRequestQueue = Volley.newRequestQueue(getApplicationContext(), new HurlStack(null, newSslSocketFactory()));

    }

    return mRequestQueue;
}

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 = getApplicationContext().getResources().openRawResource(R.raw.keystore);
        try {
            // Initialize the keystore with the provided trusted certificates
            // Provide the password of the keystore
            trusted.load(in, KEYSTORE_PASSWORD);
        } finally {
            in.close();
        }

        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(trusted);

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);

        SSLSocketFactory sf = context.getSocketFactory();
        return sf;
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

什么是raw.keystore? - WISHY
文件夹 raw,您将密钥库放在其中。 - Assem Mahrous
我该如何创建这个密钥库? - WISHY
你需要从服务器生成的证书文件中提取文本内容。 - Assem Mahrous
只分享我刚刚练习的内容。关于如何从服务器生成证书文件,我使用了这个教程:https://www.wikihow.com/Export-Certificate-Public-Key-from-Chrome - Dika
创建密钥库使用证书文件,我使用了Portecle。 - Dika

3
我以前也遇到过类似的问题,解决方法是在服务器端安装中间证书颁发机构
值得注意的是,大多数桌面浏览器访问这个服务器不会像完全未知的CA或自签名服务器证书那样出现错误。这是因为大多数桌面浏览器随着时间的推移缓存了可信的中间CA。一旦浏览器从一个站点访问并学习到一个中间CA,下一次就不需要在证书链中包括该中间CA了。
有些网站会有意地将其用于提供资源的辅助Web服务器进行此操作。例如,他们可能通过具有完整证书链的服务器提供其主要HTML页面,但是对于不包括CA的资源服务器,例如图像、CSS或JavaScript服务器,则不包括CA,可能是为了节省带宽。不幸的是,有时这些服务器可能正在提供您尝试从Android应用程序调用的Web服务,而Android不够宽容。
配置服务器以在服务器链中包括中间CA。大多数CA都提供如何在所有常见Web服务器上执行此操作的文档。如果您需要使该站点至少能在Android 4.2上与默认的Android浏览器一起工作,则只能采用此方法。
您可以按照此处提到的步骤进行操作:缺少中间证书颁发机构 另一个例子:什么是中间证书? FYI:在Android SSL连接中找不到信任锚点 浏览器可能接受根证书颁发机构,但Android SDK可能不会,因为浏览器会将它们缓存。 浏览器会缓存中间证书,并在不同的站点之间使用它们。因此,如果您缺少中间证书,一些用户将收到信任错误,而其他用户则不会。Firefox会缓存中间证书吗?

你真是救星啊,我花了两天时间检查我的代码,结果发现是服务器的问题。 - Alberto M

0

你应该接受所有的SSL证书。为了让Volley信任你的SSL,一个选项是使用以下代码,该代码基于https://developer.android.com/training/articles/security-ssl.html

// trust the SSL certificate in our smarter server
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    InputStream caInput = ctx.getAssets().open("smarter_ssl.crt");
    Certificate ca;
    try {
        ca = cf.generateCertificate(caInput);
    } finally {
        caInput.close();
    }

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

    SSLSocketFactory i = context.getSocketFactory();

    // Tell volley to use a SocketFactory from our SSLContext
    RequestQueue requestQueue = Volley.newRequestQueue(ctx.getApplicationContext(), new HurlStack(null, context.getSocketFactory()));

将 .pfx SSL 证书转换为 .crt,请按照以下步骤操作:https://www.ibm.com/support/knowledgecenter/SSVP8U_9.7.0/com.ibm.drlive.doc/topics/r_extratsslcert.html

.crt 文件应作为文本放置在资产文件夹中。


这个可以工作。至于caInput,你也可以使用ByteArrayInputStream(certificate.getBytes())。其中certificate是一个String certificate。对于证书,您需要提供x509格式的服务器证书(也称为服务器公共证书)。首先在浏览器上尝试您自签名的openssl证书。如果它可以工作,那么您就可以使用服务器证书了。此外,我使用的是Intermediate CA签名的证书,而不是自签名的证书。也许自签名的证书也可以工作,但我没有尝试过。 - Aruman
“你应该接受所有SSL证书”。不,你真的不应该。你只应该信任由你信任的CA签名的证书,或者如果自签名,则应该信任你自己的根CA证书。 - nick_j_white

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