Android / Java HTTPS No peer certificate 錯誤

開發手機應用程式時,很多時候都會使用到網路連線,透過網路上的Web service API取得App所需的資料。
而早前自己在開發Android App時,需求上必須要使用HTTPS作訪問,但使用HTTPS測試時就遇到了一個不明的錯誤,日誌上的錯誤訊息:javax.net.ssl.SSLPeerUnverifiedException: No peer certificate。

目前所使用的Web伺服器:Apache

當我使用瀏覽器查看HTTPS的請求時,並沒有任何警告提示,所以認為Web伺服器的SSL設置沒有問題,理應Android訪問應該也是沒問題才對吧。

此時Apache Web伺服器的SSL設置如下:

<VirtualHost *:443>
    ServerAdmin webmster@localhost
    DocumentRoot "/var/www/html"
    ServerName default
    ServerAlias *

    SSLEngine on
    SSLCertificateFile /etc/httpd/conf.d/ssl/server.crt
    SSLCertificateKeyFile /etc/httpd/conf.d/ssl/server.key
</VirtualHost>

經過網上一番努力爬文後,有人提到Android的証書庫是帶有startssl ca証書,而Apache默認是不帶startssl ca証書,造成Android訪問Apache的驗証就會失敗。另外,網上亦提到另一jetty Web伺服器默認是帶有startssl ca証書。為了証實是否因startssl ca証書引起,於是我便嘗試架設一個jetty Web伺服器做測試。

架設完成後,我便開始從程式碼下手,在需要使用HTTPS的地方加入如下程式碼:

private static void trustAllHosts() {
    final String TAG = "trustAllHosts";
    // Create a trust manager that does not validate certificate chains
    TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
            Log.i(TAG, "checkClientTrusted");
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
            Log.i(TAG, "checkServerTrusted");
        }

        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return new java.security.cert.X509Certificate[] {};
        }
    } };

    // Install the all-trusting trust manager
    try {
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

從程式裹可見,我為HTTPS的連線設置了自訂的証書管理物件,現在所有HTTPS的訪問都必須經過此証書管理物件。

在checkServerTrusted函式加入斷點進行除錯,查看java.security.cert.X509Certificate[] chain物件的值(chain物件為証書訊息),發現訪問兩個Web伺服器的chain值是不同的,而Apache回傳的証書訊息裹缺少了startssl ca証書,所以迷底終於解開,只要將Apache設置好startssl ca証書,Android端便可正常訪問。

加入startssl ca後,再次使用Android訪問便沒有報錯,詳細的Apache設置可到startssl官網查看教學,官網內亦有提供其它Web伺服器的設置教學。編輯後Apache Web伺服器的SSL設置如下:

<VirtualHost *:443>
    ServerAdmin webmaster@localhost
    DocumentRoot "/var/www/html"
    ServerName default
    ServerAlias *
 
    SSLEngine on
    SSLCertificateFile /etc/httpd/conf.d/ssl/server.crt
    SSLCertificateKeyFile /etc/httpd/conf.d/ssl/server.key
    SSLCertificateChainFile /usr/local/apache/conf/sub.class1.server.ca.pem
</VirtualHost>

但很多時候在開發階段,開發者只能訪問一個使用self-signed証書的Web service,所以Android一定會出現相同的錯誤訊息javax.net.ssl.SSLPeerUnverifiedException: No peer certificate,測試時實在不方便。

為了能方便開發者進行測試,在網路上有找到另一個解決方法是實作一個SSLSocketFactory來解決,此解決方式是相信所有証書,不對証書進行驗証,所以亦能正常訪問HTTPS。

MySSLSocketFactory.java 自行實作的SSLSocketFactory

package com.vvtitan.testhttps;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
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.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 org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

public class MySSLSocketFactory extends SSLSocketFactory {
    SSLContext mSSLContext = SSLContext.getInstance("TLS");

    @Override
    public Socket createSocket() throws IOException {
        return mSSLContext.getSocketFactory().createSocket();
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
        return mSSLContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    }

    public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
        super(truststore);

        TrustManager mTrustManager = new 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;
            }
        };
        mSSLContext.init(null, new TrustManager[] { mTrustManager }, null);
    }
    public static HttpClient createMyHttpClient() {
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load( null, null);

            SSLSocketFactory mSSLSocketFactory = new MySSLSocketFactory(trustStore);
            mSSLSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", mSSLSocketFactory, 443));

            ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
            return new DefaultHttpClient(ccm, params);
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        }
        return new DefaultHttpClient();
    }
}

訪問HTTPS的主程式

HttpGet request = new HttpGet("https://domain.com");
HttpClient client = MySSLSocketFactory. createMyHttpClient();

HttpParams params = client.getParams();

HttpResponse httpResponse;

try {
    httpResponse = client.execute(request);
} catch (ClientProtocolException e) {
    client.getConnectionManager().shutdown();
    e.printStackTrace();
} catch (IOException e) {
    client.getConnectionManager().shutdown();
    e.printStackTrace();
}

詳細如何實作,可參考範例(TestHttps.zip)。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

驗證碼 * Time limit is exhausted. Please reload CAPTCHA.

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料