為你的Android App實現自簽名的 SSL 證書

jopen 10年前發布 | 392K 次閱讀 Android Android開發 移動開發

介紹

網絡安全已成為大家最關心的問題. 如果你利用服務器存儲客戶資料, 那你應該考慮使用 SSL 加密客戶跟服務器之間的通訊. 隨著這幾年手機應用迅速崛起. 黑客也開始向手機應用轉移, 原因有下列3點:

  1. 手機系統各式各樣, 缺乏統一的標準.

  2. 許多程序員缺乏手機應用開發經驗.

  3. 更嚴重的是, 通過手機應用, 黑客可以得到手機用戶的隱私數據, 如:日程安排, 聯系人信息, 網頁瀏覽歷史記錄, 個人資料, 社交數據, 短信或者手機用戶所在的地理位置.

最為一個網絡安全愛好者的我, 最近花了幾個月的時間對50到60安卓應用進行安全分析, 結果發現這些應用存在許多安全漏洞.

下面我主要講一講, 怎樣才能寫出比較安全的安卓代碼.

背景

從最基本的開始講.

閱讀本文前, 最好先看下 Ranjan.D article 寫的一篇跟安卓連接有關的文章:(http://www.codeproject.com/Articles/818734/Article-Android-Connectivity).

 下列代碼用來打開一個 http 連接.

URL urlConnection = new URL("

HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();</pre>

不要在 http連接中打開:登陸頁面, 或是傳遞用戶名, 密碼, 銀行卡之類的重要個人資料. 這些重要個人數據應該通過 HTTPS 傳輸. (具體參看HTTPS).

HTTPS是什么?

HTTPS 其實就是個安全版的 http. HTTPS 能保證電子商務的交易安全. 如:網上銀行.

像 IE 或者火狐瀏覽器, 如果出現下面的掛鎖圖標.

為你的Android App實現自簽名的 SSL 證書

同時, 在瀏覽器的地址欄中以 https:// 開頭, 這表示, 你的瀏覽器跟這個網站的數據往來都是安全的.

https 跟 http 的最大區別在于 https 多加了一個保障通訊安全的層.

為你的Android App實現自簽名的 SSL 證書

像下列代碼這樣打開一個 https 連接, 可以保障這個連接的數據通訊安全.

URL urlConnection = new URL("

HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();

InputStream in = new BufferedInputStream(urlConnection.getInputStream());</pre>

HTTPS 通過 SSL/TLS 傳遞數據.

SSL/TLS:

SSL (Secure Sockets Layer) 是一種在客戶端跟服務器端建立一個加密連接的安全標準. 一般用來加密網絡服務器跟瀏覽器, 或者是郵件服務器跟郵件客戶端(如: Outlook)之間傳輸的數據.

SSL 能保障敏感信息(如:銀行卡號, 社保卡號, 登陸憑證等)的傳輸安全. 一般情況下, 數據在瀏覽器跟服務器之間的傳輸使用的是明文格式, 這種方式存在資料被竊取的風險. 如果黑客能攔截瀏覽器跟服務器之間的通訊數據, 就能看到通訊的內容. 

SSL/HTTPS and X.509 證書概述

你要是對 SSL 或 X.509 證書一無所知, 那我大概解釋下. 對于那些打算用自簽名證書(self-signed certificate)的人來說, 需要了解自簽名證書跟花錢購買機構頒發的證書有什么區別. 

首先我們需要了解下 SSL 證書究竟是個什么東東? 其實它就包含倆部分: 1) 一個身份標識, 一個用來識別身份的東西, 有點類似警察叔叔通過護照或駕照查你的身份; 2) 一個公共密鑰, 這個用來給數據加密, 而且只有證書的持有者才能解密. 簡而言之, SSL 證書就倆個功能, 身份驗證跟保障通訊過程中的數據安全.

另外還有一點很重要. 那就是一個證書可以給另外一個證書“簽字”. 用 layman 的話說就是 Bob 用他自己的證書在別的證書上蓋上 “同意” 兩個紅紅的大字. 如果你信任 Bob (當然還有他的證書), 那么你也可以信任由他簽發的證書. 在這個例子中, Bob 搖身一變, 成了證書頒發機構(Certificate Authority). 現在主流的瀏覽器都自帶一大堆受信任證書頒發機構(trusted Certificate Authorities)(比如:ThawteVerisign等).

最后我們講一講瀏覽器是怎么使用證書的. 籠統的講, 當你打開下列連接的時候 “https://www.yoursite.com” :

  1. 服務器會給瀏覽器發一個證書.

  2. 瀏覽器會對比證書中的“common name”(有時也叫 “subject”) 跟服務器的域名是否一樣. 例如, 一個從“www.yoursite.com” 網站發過來的證書就應該有一個內容是 “www.yoursite.com” 的 common name, 否則瀏覽器就會提示該證書有問題.

  3. 瀏覽器驗證證書真偽, 有點像門衛通過證件上的全息圖辨別你的證件是不是真的.  既然在現實生活中有人偽造別人的身份. 那么在網絡世界也就有人造假, 比如用你的域名“www.yoursite.com” 來偽造一個安全證書.  瀏覽器在驗證的時候, 會檢查這個證書是否是它信任機構頒發的, 如果不是, 那么瀏覽器就會提示這個證書可能有問題. 當然, 用戶可以選擇無視警告, 繼續使用.

  4. 一旦證書通過驗證 (或是用戶無視警告, 繼續使用有問題的證書), 瀏覽器就開始利用證書中的公開密鑰加密數據并傳給服務器.

TLS (SSL)中的加密

一旦服務器發過來的證書通過驗證, 瀏覽器就會利用證書中包含的公共密鑰加密某個指定的共享密鑰, 然后發給服務器. 這個加密過的共享密鑰只能用服務器的私有密鑰才能解密(非對稱加密), 別人無法解密出其中的內容. 服務器把解密出來的共享密鑰保存起來, 供本次連接會話專用. 從現在開始, 服務器跟瀏覽器之間的所有通訊信息都用這個共享密鑰加密解密(對稱加密).

為你的Android App實現自簽名的 SSL 證書

理論部分就這么多, 下面我們來看幾個例子.

在瀏覽器中打開網站 mail.live.com , 地址欄中會出現一個綠色圖標, http 也會變成 https.

單擊這個綠色圖標, 然后點證書信息連接, 就能看到下列內容.

為你的Android App實現自簽名的 SSL 證書

這是個 SSL 證書, 該證書是 Verisign 給 mail.live.cm 頒發的.

Verisign 是一個證書頒發機構, 它提示你的瀏覽器正在連接的網站是: mail.live.com, 需要跟這個網站的服務器建立一條安全連接進行通訊, 避免他人攔截或篡改瀏覽器跟服務器之間傳遞的數據.

MITM 攻擊

MITM 攻擊(MITMA)是指: 黑客攔截篡改網絡中的通訊數據

被動 MITMA 是指黑客只能竊取通訊數據, 而在主動 MITMA 中, 黑客除了竊取數據, 還能篡改通訊數據. 黑客利用 MITMA 方式攻擊手機要比攻擊臺式電腦容易的多. 這主要是因為使用手機的環境在不固定, 有些地方用手機連接上網并不安全, 尤其是那些對公眾免費開放的無線網絡熱點.

證書頒發機構(CA)

·Symantec (which bought VeriSign's SSL interests and owns Thawte and Geotrust) 38.1% 市場份額

·Comodo SSL 29.1%

·Go Daddy 13.4%

·GlobalSign 10%

Jelly bean 版本的安卓系統中, 你可以在下列路徑中找到證書頒發機構:

設置 -> 安全 -> 受信任的憑證.

Https 連接

URL url = new URL("https://www.example.com/");   
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();   
InputStream in = urlConnection.getInputStream();


如果你連接的服務器(www.example.com)傳過來的證書是由機構頒發的, 這段代碼就能正常運行.

但是如果你連的服務器用的是自己頒發的證書(self-singed certificate), 那就會出現錯誤.

什么是自簽名證書( self-signed certicates)

自簽名證書就是沒有通過受信任的證書頒發機構, 自己給自己頒發的證書.

SSL 證書大致分三類:

  • 由安卓認可的證書頒發機構(如: VeriSign), 或這些機構的下屬機構頒發的證書.

  • 沒有得到安卓認可的證書頒發機構頒發的證書.

  • 自己頒發的證書, 分臨時性的(在開發階段使用)或在發布的產品中永久性使用的兩種.

只有第一種, 也就是那些被安卓系統認可的機構頒發的證書, 在使用過程中不會出現安全提示.

為什么有人喜歡用自簽名證書 

  1. 免費. 購買受信任機構頒發的證書每年要交 100 到 500 美元不等的費用. 自簽名證書不花一分錢.

  2. 自簽名證書在手機應用中的普及率較高 (跟用電腦瀏覽網頁不同, 手機的應用一般就固定連一臺服務器.).

  3. 在開發階段寫的代碼,  測試跟發布的時候也可以用.

最近一項調查表明, 810萬個證書中, 只有 320萬個是由受信任機構頒發的. 剩余490萬證書中, 自簽名的占48%, 未知機構頒發的占33%, 而不被信任的機構頒發的證書占19%.

無獨有偶, 我的分析結果也表明, 起碼有 60% 安卓應用使用自簽證書.

個人以為, 在手機應用中使用自簽名證書沒什么不好, 既不需要花錢, 也不需要修改代碼.(注:如果你用的是機構頒發的證書, 在產品發布階段, 需要修改代碼).

但是下面的戲法的一般性的https代碼

URL url = new URL("https://www.example.com/");   
HttpsURLConnection urlConnection = (HttpsURLConnection)
url.openConnection();   
InputStream in = urlConnection.getInputStream();

如果你使用上述的代代碼去驗證你的自己簽署的證書,由于在android操作系統中自己簽署的不能通過驗證的,所以安卓應用軟件將會拋出錯誤。因此你需要書寫你自己的代碼來檢查你的自己簽署的證書。

但是在這個領域中,安卓開發者犯了一個很大的錯誤,自己簽署的證書在web開發中不是常見的,同時大多數安卓開發者來自于web開發者,所以開發者缺失在密碼學的概念的知識。

在我分析中,我發現開發者僅僅是簡單的復制、粘貼Stack Overflow和其他博客中允許你的應用默認信任所以證書的答案。即使大多說的答案表述僅僅在測試模式下可以使用,但是開發者簡單地復制代碼,將會導致應用軟件在遭到中間件攻擊和session黑客攻擊是,表現的非常的脆弱!

例子:

http://stackoverflow.com/questions/2703161/how-to-ignore-ssl-certificate-errors-in-apache-httpclient-4-0

http://stackoverflow.com/questions/2012497/accepting-a-certificate-for-https-on-android?lq=1

http://www.caphal.com/android/using-self-signed-certificates-in-android/#toc_3

http://stackoverflow.com/questions/2642777/trusting-all-certificates-using-httpclient-over-https

在使用自己簽署的證書時一般性的錯誤

信任所以的證書

TrustManager的主要責任是去決定提出的認證證書應該是可信任的。如果證書是不可信任的,那么連接將會被終止。去認證遠程的安全套接字識別,你需要用一個或者多個TrustManager(s)初始化SSLContext對象。

import org.apache.http.conn.ssl.SSLSocketFactory;
public class MySSLSocketFactory extends SSLSocketFactory {
    SSLContext sslContext = SSLContext.getInstance("TLS");

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

        TrustManager tm = new X509TrustManager() {             public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {             }

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

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

        sslContext.init(null, new TrustManager[] { tm }, null);    }

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

    @Override     public Socket createSocket() throws IOException {         return sslContext.getSocketFactory().createSocket();     } }</pre>

我發現,80到100個應用軟件中,在20到25個應用中實現了上述的代碼。

在上述的TrustManager接口中,可以實現信任所以的證書,不論是誰簽署的或者即使他們發布的任何主題。這個接口將會允許接受ANY證書。接受任何的證書將會危害數據的完整性、安全性等等。

在上述的例子中,檢查客戶端可信任性,獲得接受事件,檢查服務器端可信任性是三點重要的功能。每一位開發者都應該留意這三點功能的實現上。但是卻很少有開發者從不同的網站中搜索、負責上述的功能。

允許所有的主機名。

忘記檢查證書是否在這個地址發布是有可能的。當證書接受了example.com的服務器,那么另外的一個域名也將被接受。

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://www.paypal.com&rdquo; HttpPost httpPost = new HttpPost(url); HttpResponse response = httpClient.execute(httpPost);

HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);</pre>

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

上述的代碼中,即使是錯誤的實現,也將會接受對任何域名的任何CA證書聲明。

Mixed-Mode/No SSL.

應用軟件開發者在相同的應用中使用最大安全、不安全的連接,或者不使用SSL將是免費的。這不是直接的SSL聲明,但是和提到的沒有向外的簽署個有關,同時對于一般軟件的使用者,檢查是否使用一個安全的連接是不可能的。這將會為例如SSL剝離,或者像FireSheep這樣的攻擊開后門。

SSL剝離是另外的一種可以使MITMA登陸并抵制SSL連接方式。利用使用最大HTTP和HTTPS應用。SSL剝離依賴大量建立在點擊鏈接、或者來自于沒有SSL重定向保護的網站SSL連接。在SSL剝離中,Mallory用HTTPS://取代了沒有保護的網站中http:// 鏈接中。因此,除非使用者注意到了鏈接被篡改了,Mallory可以完全地規避SSL保護。這樣的攻擊主要與瀏覽器應用、或者原生使用安卓WebView應用有關。

更多關于SSL剝離的信息:

http://security.stackexchange.com/questions/41988/how-does-sslstrip-work

http://www.thoughtcrime.org/software/sslstrip/


證書鎖定

直接在代碼中固定寫死使用某個服務器的證書. 然后在應用中使用自己定義的信任存儲(trust store)代替手機系統自帶的那個, 去連接指定的服務器.

這樣做的好處是, 我們既能使用自簽名證書, 又不需要額外安裝其他證書.

優勢

  • 安全性提升 - 采用這種方式, 應用不再依賴系統自帶的信任存儲(trust store). 使得破解這種應用變得復雜: 首先你要反編譯, 修改完后, 還要重新編譯.  關鍵是你不可能使用應用作者原先用的那個 keystore 文件重新頒發證書.

  • 成本降低 - 證書鎖定方式讓我們可以在自己的服務器上使用免費的自簽名證書,  調用自己寫的 API. 雖說復雜了點, 可是像這種既不花錢, 還能提高應用安全的好事上哪找去?

缺點

  • 適應性較差 - 一旦 SSL 證書出現變動, 應用也要跟著升級. 再發布到 Google Play. 然后祈禱用戶能都升級到最新版本.

安卓的 SSLContext 自帶的 TrustManager 無法讓本文示例中提到的自簽名證書通過驗證. 解決的辦法是自己定義一個 TrustManager 類. 然后用這個類去驗證自簽名證書. 

先把證書加載到 KeyStore, 然后用 KeyStore 生成一個 TrustManager 數組, 最后再用這個 TrustManager 數組創建 SSLContext.

本文的應用把服務器的證書直接存進應用的資源.(畢竟這個文件是所有用戶都共用的, 而且也不會經常改動), 當然你可以把它存到別的地方.

實現的步驟

第1步: 創建自簽名證書和.bks 文件

1) 

創建 BKS 或者 keystore, 需要用到下面這個文件,bcprov-jdk15on-146.jar, 版本很多, 我用的是這個:http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar, 下載后, 把文件存到 C:\codeproject.

然后用 Keytool 生成 keystore 文件.(keytool 是 Java SDK 自帶的文件, 跟javac 放在同一個目錄下)在命令提示符窗口中輸入 keytool 就能看到這個工具的各種選項說明. 或者輸入下列路徑運行.

"C:\Program Files (x86)\Java\jre7\bin>keytool".

2)

下面是用 keytool 生成 keysotre 文件的命令.  要是這個文件已經存在, 這一步可以忽略.

keytool -genkey -alias codeproject -keystore C:\codeproject\codeprojectssl.keystore -validity 365

這行命令創建一個別名為 code project 的密鑰(key), 生成的文件名是 codeprojectssl.keystore. 執行文件生成過程中會要求輸入密鑰(key)跟keystore的密碼諸如此類的東東. 這里需要注意下, 當要求你錄入 Common name 的時候, 要填你的主機名. 本文例子用的是: codeproject.com

3)

keytool -export -alias codeproject -keystore C:\codeproject\codeprojectssl.keystore -file C:\codeproject\codeprojectsslcert.cer

這行命令將密鑰(key)從 .keystore 文件導入 .cer 文件.

4) 

keytool -import -alias codeproject -file C:\codeproject\codeprojectsslcert.cer -keystore C:\codeproject\codeprojectssl.bks -storetype BKS -providerClass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath C:\codeproject\bcprov-jdk15on-146.jar

搞定! 現在, 全部 .bks 文件都生成了. 稍后將這些文件復制到安卓應用中. 連接那些使用自簽名證書的服務器的時候會用到.

第2步

把 .keystore 文件復制到 /androidappdir/res/raw/

第3步

創建一個新類: MyHttpClient,  繼承 DefaultHttpClient 類. 這個新類在驗證SSL 證書的時候, 會自動加載我們自己創建的 keystore 文件, 而不是安卓自帶的那個. 只要證書跟服務器匹配上了就沒問題. 代碼如下:

import java.io.InputStream;
import java.security.KeyStore;

import android.content.Context;

public class MyHttpClient extends DefaultHttpClient {     private static Context context;    public static void setContext(Context context) {   MyHttpClient.context = context;  }

 public MyHttpClient(HttpParams params) {   super(params);  }

 public MyHttpClient(ClientConnectionManager httpConnectionManager, HttpParams params) {   super(httpConnectionManager, params);  }

 @Override     protected ClientConnectionManager createClientConnectionManager() {         SchemeRegistry registry = new SchemeRegistry();         registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));                  // 用我們自己定義的 SSLSocketFactory 在 ConnectionManager 中注冊一個 443 端口           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");             // 從資源文件中讀取你自己創建的那個包含證書的 keystore 文件                          InputStream in = MyHttpClient.context.getResources().openRawResource(R.raw.codeprojectssl); //這個參數改成你的 keystore 文件名             try {                 // 用 keystore 的密碼跟證書初始化 trusted                                             trusted.load(in, "這里是你的 keystore 密碼".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);         }     } }</pre>

MyHttpClient 類的調用代碼如下:

// Instantiate the custom HttpClient
DefaultHttpClient client = new MyHttpClient(getApplicationContext());
HttpGet get = new HttpGet("https://www.google.com");
// 以 GET 方式讀取服務器返回的數據
HttpResponse getResponse = client.execute(get);
HttpEntity responseEntity = getResponse.getEntity();

這是我在 CodeProject 上的發布的處女作.

祝大家開心編程, 安全第一 為你的Android App實現自簽名的 SSL 證書 .

參考文獻

http://www.thoughtcrime.org/blog/authenticity-is-broken-in-ssl-but-your-app-ha/
http://security.stackexchange.com/questions/29988/what-is-certificate-pinning
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
https://tools.ietf.org/html/draft-ietf-websec-key-pinning-20
https://media.blackhat.com/bh-us12/Turbo/Diquet/BH_US_12_Diqut_Osborne_Mobile_Certificate_Pinning_Slides.pdf
http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#HowSSLWorks

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!