轉載請註明出處 http://www.paraller.com
原文排版地址 點擊獲取更好閱讀體驗php
轉載:http://xhrwang.me/2015/06/06/https-and-android.html 部分修改
html
Google 在 Android 6.0 Changes 中聲稱在 Android 6.0 系統中,移除了 Apache HttpClient 組件「java
Android 6.0 release removes support for the Apache HTTP client. If your app is using this client and targets Android 2.3 (API level 9) or higher, use the HttpURLConnection class instead. This API is more efficient because it reduces network use through transparent compression and response caching, and minimizes power consumption. To continue using the Apache HTTP APIs, you must first declare the following compile-time dependency in your build.gradle file:android
因此,爲了適配最新的 Android 6.0 系統,做爲開發者,若是以前使用了 Apache HttpClient 開發的網絡通訊模塊,是時候使用 URLConnection 以及 Derived 類型來從新實現,畢竟調整後能獲得上面 Google 提到的各類好處,並且不須要依賴一個額外的包。git
在咱們使用智能移動設備的時候,不少時候咱們可能處於不安全的 Wifi 環境中,考慮到用戶數據在傳輸過程當中的安全性,Https
協議愈來愈成爲網絡通訊的主流,可是使用 Https
的時候若是實現有誤,仍是避免不了 MITMA (Man in the middle attack),國外有篇論文 Detection of SSL-related securityvulnerabilities in Androidapplications 對這種狀況做了很好的總結,推薦你們看看。並且 SSL Vulnerabilities at Large 文章通過統計發現 73% 的 Android 應用程序沒有正確地在 Https
通訊過程當中進行證書驗證。算法
這篇文章就是想討論一下如何正確地使用 HttpsURLConnection
實現 Https
通訊。除了本文,Android security - Implementation of Self-signed SSL certificate for your App. 也很是推薦,但遺憾的是該文最後給出的完整實例使用了 Apache HttpClient 的實現方式,不過該文對 Android 應用開發中如何正確處理 Https
通訊作了很是好的分析。flask
咱們通常常見的證書鏈分爲兩種:瀏覽器
受信任的根證書頒發機構
頒發的證書(CRT 文件),因爲這種狀況下一旦 Root CA 證書遭到破壞或者泄露,提供這個 Certificate Authority 的機構以前頒發的證書就所有失去安全性了,須要所有換掉,對這個 CA 也是毀滅性打擊,如今主流的商業 CA 都提供三級證書。受信任的根證書頒發機構
下的 中級證書頒發機構
頒發的證書,這樣 ROOT CA 就能夠離線放在一個物理隔離的安全的地方,即便這個 CA 的中級證書被破壞或者泄露,雖而後果也很嚴重,但根證書還在,能夠再生成一箇中級證書從新頒發證書,並且這種狀況對 HTTPS 的性能和證書安裝過程也沒有太大影響,這種方式也基本成爲主流作法。咱們的互聯網就是運行在這個基於信任關係的基礎上,國際上的 受信任的根證書頒發機構
是有限的幾個機構,具體信息能夠參考 維基百科的介紹。安全
生成使用 RSA 非對稱加密類型的私鑰,使用 DES3 算法,輸出 OpenSSL 格式,採用 2048 位強度。server.key是文件名,生成過程須要提供一個至少四位的密碼。
openssl genrsa -des3 -out server.key 2048服務器
在部署支持 HTTPS 網站的時候 Web Server 每次啓動都須要提供使用的私鑰密碼,可使用下面的命令去除剛生成的私鑰密碼:
openssl rsa -in server.key -out server_nopwd.key
openssl req -new -key server_nopwd.key -out server.csr
在這個過程當中須要提供像國家、地區、組織名稱以及 E-mail 等信息,對於用於 HTTPS 的 CSR 來講,Common Name
必須和網站域名一致,以便以後進行 Host Name 校驗,因此 Common Name
的選擇比較重要,能夠選擇 Single Name
或者 WildCard
類型的,兩者區別可見 Choosing the SSL Certificate Common Name。上面的命令執行時會須要輸入一些信息:
Country Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:BJ Locality Name (eg, city) []:BJ Organization Name (eg, company) [Internet Widgits Pty Ltd]:OrgName Organizational Unit Name (eg, section) []:OrgUnit Common Name (eg, YOUR name) []:www.DOMAIN.com Email Address []:Name@DOMAIN.com
Self-Signed Certificate 自簽名證書,若是隻是用於好比應用 API 接口,不須要用於可經過瀏覽器訪問的網頁展現,爲了節省開支或者僅僅是內部使用能夠考慮。
openssl x509 -req -days 365 -in server.csr -signkey server_nopwd.key -out server.crt
提交 CSR 文件併購買授信中級證書機構頒發的證書
openssl x509 -md5 -days 3560 -req -CA ca.crt -CAkey ca.key -CAcreateserial -CAserial ca.srl -in server.csr -out server.crt
有的 CA 簽名後會提供兩個 CRT 文件,一個是對提交的 CSR 文件簽名後的 CRT,一個是 CA 本身的 Intermediate CRT 文件,這個 CRT 文件是公開的,好比 Symantic 的常見 Intermediate CRT 就能夠從 Symantec Class 3 Secure Server CA - G4 下載到,GoDaddy 的全部不一樣格式的 CRT 文件都公開在 GoDaddy Repository,爲了讓配置的 Https 網站被瀏覽器認爲是可信的,這兩個 CRT 文件能夠合併後配置到 Web Server 中,這至關於在 Web Server 服務器上安裝了該 CA 的 Intermediate CRT 證書。合併方式能夠參考 How do I make my own bundle file from CRT files?
使用 CRT 文件和 私鑰配置 HTTPS 網站服務
這裏的具體操做方式取決於使用的 Web Server 類型,好比 Apache 和 Nginx 有本身的配置方法,就不列了,各個 Web Server 的文檔都有詳細說明,GoDaddy 的文檔 INSTALL SSL CERTIFICATES 對主流的 Web Server 如何進行 Https
配置做了很是全面的總結。
這裏咱們可使用一個可用於測試的 Python Flask 實現的 Https Web Server:
from OpenSSL import SSL from flask import Flask from flask import render_template app = Flask(__name__) context = SSL.Context(SSL.SSLv23_METHOD) context.use_privatekey_file('/Users/rwang/testssl/test.key') context.use_certificate_file('/Users/rwang/testssl/test.crt') @app.route("/") def hello(): return "Howdy, there!", 200 if __name__ == "__main__": app.run(host='127.0.0.1',port=8443, debug = True, ssl_context=context)
啓動這個測試服務器,就能夠從瀏覽器中經過 https://127.0.0.1:8443
這個地址訪問了,若是你使用了 Self signed Certificate
,瀏覽器會提示危險,選擇仍然繼續就能夠看到 Howdy, there!
的歡迎語了。注意,若是須要經過在同一個局域網內的 Android 設備訪問這個測試服務器,請把 127.0.0.1
替換爲本機在局域網內的 ip。
這裏分兩種狀況來討論。
Increased security - with pinned SSL certificates, the app is independent of the device’s trust store. Compromising the hard coded trust store in the app is not so easy - the app would need to be decompiled, changed and then recompiled again - and it can’t be signed using the same Android keystore that the original developer of the app used.
**Reduced costs** - SSL certificate pinning gives you the possibility to use a self-signed certificate that can be trusted. For example, you’re developing an app that uses your own API server. You can reduce the costs by using a self-signed certificate on your server (and pinning that certificate in your app) instead of paying for a certificate. Although a bit convoluted, this way, you’ve actually improved security and saved yourself some money.
固然,文中也提到使用自簽名證書也有不足的地方:
Less flexibility - when you do SSL certificate pinning, changing the SSL certificate is not that easy. For every SSL certificate change, you have to make an update to the app, push it to Google Play and hope the users will install it.
使用自簽名證書,須要自定義 TrustManager
:
class MyTrustManager implements X509TrustManager { X509Certificate cert; MyTrustManager(X509Certificate cert) { this.cert = cert; } @Override // for server only public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 咱們在客戶端只作服務器端證書校驗。 } @Override // only trust the given certificate or certificate issued by it public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 確認服務器端證書和代碼中 hard code 的 CRT 證書相同。 if (chai[0].equals(this.cert)){ if(Utils.DEBUG){ Log.i(Utils.DEBUG_TAG, "checkServerTrusted Certificate from server is valid!"); } return;// found match } throw new CertificateException("checkServerTrusted No trusted server cert found!"); } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }
而後使用其定義 SSLContext
實例:
SSLContext sc = SSLContext.getInstance("TLS"); TrustManager tm = new MyTrustManager(readCert(certStr)); sc.init(null, new TrustManager[]{ tm }, null); `</pre> </div> 其中,`readCert` 方法的實現爲: <div class="highlighter-rouge"><pre class="highlight js-evernote-checked" data-evernote-id="212">` private static X509Certificate readCert(String cer) { if (cer == null || cer.trim().isEmpty()) return null; InputStream caInput = new ByteArrayInputStream(cer.getBytes()); X509Certificate cert = null; try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); cert = (X509Certificate) cf.generateCertificate(caInput); } catch (Exception e) { if (Utils.DEBUG) { e.printStackTrace(); } } finally { try { if (caInput != null) { caInput.close(); } } catch (Throwable ex) { } } return cert; }
最後使用 HttpsURLConnection
進行網絡通訊:
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); conn.setSSLSocketFactory(sc.getSocketFactory()); // By default, this implementation of HttpURLConnection requests that servers use gzip compression // and it automatically decompresses the data for callers of getInputStream(). // The Content-Encoding and Content-Length response headers are cleared in this case. // Gzip compression can be disabled by setting the acceptable encodings in the request header: // http://developer.android.com/reference/java/net/HttpURLConnection.html conn.setRequestProperty("Accept-Encoding", ""); StringBuffer response = new StringBuffer(); OutputStream os = null; BufferedReader rd = null; InputStream is = null; int statusCode = -1; try { conn.setRequestMethod("POST"); os = conn.getOutputStream(); os.write(payload); os.close(); // Get Response statusCode = conn.getResponseCode(); InputStream is; if(statusCode > HttpURLConnection.HTTP_BAD_REQUEST){ is = conn.getErrorStream(); }else{ is = conn.getInputStream(); } rd = new BufferedReader(new InputStreamReader(is)); String line; while ((line = rd.readLine()) != null) { response.append(line); response.append('\n'); } } catch (Throwable e) { if (Utils.DEBUG) { e.printStackTrace(); } } finally { try { if (is != null) { is.close(); } } catch (Throwable ex) { } try { if (os != null) { os.close(); } } catch (Throwable ex) { } try { if (rd != null) { rd.close(); } } catch (Throwable ex) { } try { if (conn != null) { conn.disconnect(); } } catch (Throwable ex) { } }
須要注意的是:
SSLContext
將被用於本次 Https 通訊,不會影響應用中其餘可能的 Https 通訊。使用通過 CA 認證的證書
若是服務器端正確配置了使用 CA 認證後的證書,Android 客戶端應用程序能夠直接使用 `HTTPSURLConnection` 訪問:
URL url = new URL("https://www.example.com/"); HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); InputStream in = urlConnection.getInputStream();
若是須要作更嚴格的驗證,也能夠這樣自定義 TrustManager
:
class MyTrustManager implements X509TrustManager { X509Certificate cert; MyTrustManager(X509Certificate cert) { this.cert = cert; } @Override // for server only public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 咱們在客戶端只作服務器端證書校驗。 } @Override // only trust the given certificate or certificate issued by it public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 確認服務器端證書的 Intermediate CRT 和代碼中 hard code 的 CRT 證書主體一致。 if (!chain[0].getIssuerDN().equals(certificate.getSubjectDN())) { throw new CertificateException("Parent certificate of server was different than expected signing certificate"); } try { // 確認服務器端證書被代碼中 hard code 的 Intermediate CRT 證書的公鑰簽名。 chain[0].verify(certificate.getPublicKey()); // 確認服務器端證書沒有過時 chain[0].checkValidity(); } catch (Exception e) { throw new CertificateException("Parent certificate of server was different than expected signing certificate"); } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }
思路就是在代碼中 hard code CA 認證後交付的 Intermediate CRT 文件做爲客戶端證書對服務器端證書進行校驗。根據 開發安全的Android應用 提出的觀點,能夠避免最終用戶證書有效期可能比較短的問題。
註釋:
證書失效能夠根據Intermediate CRT 從新續期
以後使用 HttpsURLConnection
的方式和使用自簽名證書時相同就不特別說明了。