安卓 java.security.cert.CertPathValidatorException:找不到证书路径的信任锚点

18
有三个主机用于android应用程序的身份验证和授权。最终主机是REST API。第一次使用Oauth身份验证和授权过程时,它可以正常工作。
但是,如果用户在登录并访问REST API提供的服务后关闭应用程序,然后再次打开应用程序,就会出现此问题。在这种情况下,身份验证和授权过程不会发生,只有REST API。这导致了java.security.cert.CertPathValidatorException异常,但在第一次使用(登录然后使用应用程序)期间它是有效的。
有人能解释一下这个异常背后的情景以及应用程序出了什么问题吗?根据this SO answer,如果忽略证书异常,则可以解决此问题。
SSLSocketFactory sslSocketFactory = null;

        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            // Initialise the TMF as you normally would, for example:
            try {
                tmf.init((KeyStore)null);
            } catch(KeyStoreException e) {
                e.printStackTrace();
            }
            TrustManager[] trustManagers = tmf.getTrustManagers();

            final X509TrustManager origTrustmanager = (X509TrustManager)trustManagers[0];

            // Create a trust manager that does not validate certificate chains
            TrustManager[] wrappedTrustManagers = new TrustManager[]{
                    new X509TrustManager() {
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return origTrustmanager.getAcceptedIssuers();
                        }

                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                            try {
                                origTrustmanager.checkClientTrusted(certs, authType);
                            } catch(CertificateException e) {
                                e.printStackTrace();
                            }
                        }

                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                            try {
                                origTrustmanager.checkServerTrusted(certs, authType);
                            } catch(CertificateException e) {
                                e.printStackTrace();
                            }
                        }
                    }
            };
            //TrustManager[] trustAllCerts = TrustManagerFactory.getInstance("SSL").getTrustManagers();

            // Install the all-trusting trust manager
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, wrappedTrustManagers, new java.security.SecureRandom());
            // Create an ssl socket factory with our all-trusting manager
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
        }
        return sslSocketFactory;

我正在使用Okhttp 3进行http请求。任何建议都可以帮助解决问题。如果我使用上面的代码片段,是否会存在安全问题?会影响应用程序的安全性吗?


注意:你的 checkServerTrusted 实现是无用的,因为你捕获并忽略了 CertificateException。因此所有服务器证书(受信任和不受信任的)都会被接受! - Robert
@Robert 感谢您的澄清。是的,从安全角度来看,这是不安全且无用的。我已经找到了正确的解决方案,如我的答案所述。 - Ruwanka De Silva
3个回答

28
我回答这个问题是为了让其他人了解情况和根据Android开发者网站提供的解决方案。我使用自定义信任管理器来解决了这个问题。
问题出在服务器证书上,它缺少中间证书颁发机构。然而,在第一个流程中,证书路径已经完成,结果是成功的证书路径验证。 Android开发者网站 上有一个解决方案。它建议使用自定义信任管理器来信任此服务器证书,或者建议服务器在服务器链中包含中间CA。
自定义信任管理器。来源:https://developer.android.com/training/articles/security-ssl.html#UnknownCa
// 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 = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} 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);
// Tell the okhttp to use a SocketFactory from our SSLContext
OkHttpClient okHttpClient client = new OkHttpClient.Builder().sslSocketFactory(context.getSocketFactory()).build();
更新:我的问题在服务器端添加中间证书机构到证书链后得到解决。这是最好的解决方案,将证书捆绑到应用程序需要在证书过期或与证书管理相关的任何其他问题上更新应用程序。

更新:2017年3月9日我发现加载证书文件的最简单方法是使用原始资源。

InputStream caInput = new BufferedInputStream(context
                .getResources().openRawResource(R.raw.certfilename));

certfilename是放置在resources/raw文件夹中的证书文件。另外,okhttp的sslSocketFactory(SSLSocketFactory sslSocketFactory)已经被弃用,建议使用okhttp api文档中提供的方法。

此外,从服务器获取证书时最好使用openssl。

openssl s_client -connect {server-address}:{port} -showcerts

因为我曾经从Firefox中获取该内容,但遇到了被病毒防护程序更改的情况。

2
  1. Paste your cert.pem in raw folder

  2. Create a method

    private SSLSocketFactory getSSLSocketFactory(){
        try {
            CertificateFactory cf;
            cf = CertificateFactory.getInstance("X.509");
    
            Certificate ca;
            InputStream cert = context.getResources().openRawResource(R.raw.cert);
            ca = cf.generateCertificate(cert);
            cert.close();
    
            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore   = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);
    
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);
    
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
    
            return sslContext.getSocketFactory();
    
        }
        catch (Exception e){
            return null;
        }
    }
    
  3. Call like this

    final OkHttpClient client = new OkHttpClient();
     //pass getSSLSocketFactory() in params
     client.setSslSocketFactory(getSSLSocketFactory());
    
     String appURl = context.getString(R.string.apis_app_url);
    
     final RestAdapter restAdapter = new RestAdapter.Builder()
             .setEndpoint(appURl).setClient(new OkClient(client)).
                     build();
    

setSslSocketFactory已经被弃用。 - Programer Beginner

0
在我的情况下,答案是ssl证书文件的一部分缺失(我在服务器上有几个文件),解决方案只是将它们全部复制并粘贴到一个文件中,并在nginx配置文件中提供该新文件的路径。

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