為什麼需要 SSL Pinning?
行動應用安全威脅:
- 中間人攻擊(MITM):攻擊者攔截應用與伺服器之間的通訊,竊取敏感數據
- 偽造憑證攻擊:惡意軟體可能在設備上安裝偽造的根憑證
- 公共 Wi-Fi 風險:不安全的網路環境容易被竊聽
- 企業內部風險:某些企業防火牆會解密 HTTPS 流量進行檢查
SSL Pinning 的價值:
- 額外安全層:即使設備的信任鏈被破壞,應用仍能驗證伺服器身份
- 防止偽造憑證:只信任指定的憑證或公鑰,拒絕其他來源
- 符合合規要求:金融、醫療等行業的安全規範要求
- 保護敏感數據:確保用戶隱私和交易安全
什麼是 SSL Pinning?
SSL Pinning 是一種安全技術,它將伺服器的 憑證 或 公鑰 內嵌於應用程式中,讓應用只信任指定伺服器的憑證,即使攻擊者偽造憑證也無法進行中間人攻擊(MITM)。Pinning 可分為以下兩種方式:
- 憑證 Pinning(Certificate Pinning):將伺服器的整個憑證內嵌於應用中進行校驗
- 公鑰 Pinning(Public Key Pinning):提取伺服器憑證中的公鑰,僅校驗該公鑰是否匹配
什麼是自定義信任鏈?
自定義信任鏈 通常用於替換 Android 的預設憑證驗證機制。這種方法允許您指定應用信任的根憑證或中繼憑證(Intermediate Certificate)。
常見使用場景:
- 自簽名憑證:伺服器使用的是自簽名憑證(開發或測試環境)
- 企業內部 CA:使用企業內部的憑證頒發機構(CA)
- 特殊中繼憑證:伺服器的憑證信任鏈中包含特定的中繼憑證
- 測試環境隔離:開發環境使用不同的憑證體系
準備工作
所需工具與資源:
- 伺服器憑證:獲取
.cer或.crt格式的伺服器憑證 - OpenSSL:用於檢查和提取憑證相關資訊
- macOS/Linux:通常已預裝
- Windows:從 OpenSSL 官網下載
- Android 專案環境:
- Android Studio 最新版本
- OkHttp 庫(版本 4.x 或以上)
- Kotlin 或 Java 開發環境
實作步驟
步驟 1:檢查伺服器憑證
首先,檢查伺服器的憑證,確保域名、有效期及簽發機構符合需求。
使用以下 OpenSSL 指令:
openssl x509 -in <your_certificate.cer> -text -noout
確認重點:
- Subject:檢查域名(CN=your.domain.com)是否正確
- Issuer:確認憑證的簽發機構(如 Let’s Encrypt, DigiCert)
- Validity:檢查憑證的有效期(Not Before / Not After)
- Subject Alternative Name:確認支援的所有域名
範例輸出:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 04:00:00:00:00:01:54:a8:30:f2:d5
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=Let's Encrypt, CN=R3
Validity
Not Before: Jan 1 00:00:00 2025 GMT
Not After : Apr 1 23:59:59 2025 GMT
Subject: CN=api.example.com
步驟 2:提取伺服器公鑰
如果您選擇使用 公鑰 Pinning(建議方式),需要提取伺服器憑證中的公鑰:
openssl x509 -in <your_certificate.cer> -pubkey -noout > public_key.pem
檢查提取的 public_key.pem 檔案:
cat public_key.pem
確保內容類似:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
步驟 3:生成公鑰的 SHA-256 指紋
生成伺服器公鑰的 SHA-256 指紋,供 SSL Pinning 使用:
openssl x509 -in <your_certificate.cer> -pubkey -noout |
openssl rsa -pubin -outform DER |
openssl dgst -sha256 -binary |
base64
輸出的指紋範例:
sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz=
記錄此指紋,稍後在 Android 程式中配置。
重要提示: 建議同時記錄主要憑證和備份憑證的指紋,以便憑證輪換時應用仍能正常運作。
步驟 4:配置憑證至 Android 專案
1. 創建資源目錄:
mkdir -p app/src/main/res/raw
2. 複製憑證檔案:
cp <your_certificate.cer> app/src/main/res/raw/server_cert.cer
檔案命名建議:
- 使用描述性名稱:
server_cert.cer,api_certificate.cer - 避免特殊字元,僅使用小寫字母、數字和底線
- 如有多個憑證,使用
cert_primary.cer,cert_backup.cer
步驟 5:實現自定義信任鏈(Kotlin)
如果您需要使用特定的中繼憑證(如 eCA1-to-HRCA1.crt),可以使用以下代碼自定義信任鏈:
import java.security.KeyStore
import java.security.cert.CertificateFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient
fun createCustomTrustManager(context: Context): X509TrustManager {
// 初始化 TrustManagerFactory
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
// 創建 KeyStore
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
// 加載自定義憑證
val certificateFactory = CertificateFactory.getInstance("X.509")
val inputStream = context.resources.openRawResource(R.raw.server_cert)
try {
val certificate = certificateFactory.generateCertificate(inputStream)
keyStore.setCertificateEntry("server", certificate)
} finally {
inputStream.close()
}
// 初始化 TrustManager
trustManagerFactory.init(keyStore)
val trustManagers = trustManagerFactory.trustManagers
return trustManagers[0] as X509TrustManager
}
fun createSecureClient(context: Context): OkHttpClient {
val trustManager = createCustomTrustManager(context)
// 設置自定義的 SSLContext
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
// 建立 OkHttpClient
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
}
步驟 6:實現 SSL Pinning(Kotlin)
使用 OkHttp 配置 SSL Pinning:
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
fun createPinnedClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
// 主要憑證
.add(
"api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
// 備份憑證(建議)
.add(
"api.example.com",
"sha256/BackupCertificateHashGoesHere1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
步驟 7:結合自定義信任鏈與 SSL Pinning
fun createFullySecureClient(context: Context): OkHttpClient {
val trustManager = createCustomTrustManager(context)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
val certificatePinner = CertificatePinner.Builder()
.add(
"api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
.build()
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.certificatePinner(certificatePinner)
.build()
}
Java 實作範例
如果您的專案使用 Java,以下是對應的實作:
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
public class SecureClientBuilder {
public static OkHttpClient createSecureClient(Context context) {
try {
// 加載憑證
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream certInputStream = context.getResources()
.openRawResource(R.raw.server_cert);
Certificate ca = cf.generateCertificate(certInputStream);
certInputStream.close();
// 建立 KeyStore
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", ca);
// 建立 TrustManager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init(keyStore);
// 建立 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
// 建立 Certificate Pinner
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz=")
.build();
// 建立 OkHttpClient
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(),
(X509TrustManager) tmf.getTrustManagers()[0])
.certificatePinner(certificatePinner)
.build();
} catch (Exception e) {
throw new RuntimeException("Failed to create secure client", e);
}
}
}
測試與驗證
測試 SSL Pinning 是否生效:
- 正常連線測試:
val client = createPinnedClient() val request = Request.Builder() .url("https://api.example.com/test") .build() client.newCall(request).execute().use { response -> println("Response: ${response.body?.string()}") } - 錯誤憑證測試:
- 修改 Pinning 的 SHA-256 指紋為錯誤值
- 預期結果:應該拋出
SSLPeerUnverifiedException
- 使用 Charles Proxy 測試:
- 安裝 Charles 的根憑證到設備
- 嘗試攔截應用流量
- 預期結果:連線失敗,無法攔截
常見問題與解決方案
問題 1:為什麼需要特定憑證(如 eCA1-to-HRCA1.crt)?
原因:
- 該憑證可能是伺服器憑證信任鏈的一部分(例如中繼憑證)
- 如果未包含此憑證,應用將無法成功建立信任鏈
- Android 系統的預設信任庫可能不包含某些企業或特殊 CA
解決方案:
- 向伺服器管理員索取完整的憑證鏈
- 將所有中繼憑證都加入到 KeyStore 中
- 使用
openssl s_client命令檢查伺服器的完整憑證鏈
問題 2:SSL Pinning 與自定義信任鏈的差異是什麼?
比較:
| 特性 | SSL Pinning | 自定義信任鏈 |
|---|---|---|
| 驗證對象 | 憑證或公鑰的指紋 | 完整的憑證鏈條 |
| 靈活性 | 較低,需更新應用 | 較高,可動態管理憑證 |
| 安全性 | 非常高,精確匹配 | 高,依賴信任鏈 |
| 使用場景 | 防止 MITM 攻擊 | 自簽名或企業 CA |
建議: 可以同時使用兩者,提供雙重保護。
問題 3:選擇公鑰 Pinning 還是憑證 Pinning?
公鑰 Pinning(推薦):
- ✅ 更靈活,憑證更新時公鑰可保持不變
- ✅ 減少應用更新頻率
- ✅ 支援憑證輪換(Rotation)
憑證 Pinning:
- ✅ 更加嚴格,完全匹配憑證
- ❌ 憑證更新後必須更新應用
- ❌ 維護成本較高
問題 4:憑證過期或更新時怎麼辦?
解決方案:
- 準備多個 Pin:
val certificatePinner = CertificatePinner.Builder() .add("api.example.com", "sha256/current_cert_hash") .add("api.example.com", "sha256/backup_cert_hash") .build() - 實施 Pin 更新機制:
- 在伺服器端提供 Pin 配置 API
- 應用啟動時動態更新 Pin 列表
- 使用 Firebase Remote Config 或類似服務
- 監控與告警:
- 設置憑證過期提醒(提前 30-60 天)
- 監控 SSL Pinning 失敗率
- 準備緊急更新流程
問題 5:如何在開發環境中測試?
解決方案:
- 使用 Build Variants 區分開發和生產環境
- 開發環境停用 Pinning 或使用開發憑證
- 使用條件編譯:
val client = if (BuildConfig.DEBUG) { // 開發環境:不啟用 Pinning OkHttpClient.Builder().build() } else { // 生產環境:啟用 Pinning createPinnedClient() }
安全最佳實踐
- 使用公鑰 Pinning:提供更好的靈活性
- 準備備份 Pin:至少配置 2-3 個有效的 Pin
- 定期檢查憑證:設置自動化監控
- 實施 Pin 更新機制:透過遠端配置動態更新
- 錯誤處理與降級:Pin 驗證失敗時提供適當的錯誤訊息
- 日誌記錄:記錄 Pinning 失敗事件,用於安全分析
- 結合多層防護:同時使用 SSL Pinning 和應用層加密
實際應用案例
金融應用:
- 使用 SSL Pinning 保護交易 API
- 結合設備指紋和生物識別
- 定期輪換憑證和 Pin
企業應用:
- 使用自定義信任鏈信任企業 CA
- 配置內部網路的自簽名憑證
- 實施憑證透明度(Certificate Transparency)
結論
實現 SSL Pinning 與自定義信任鏈是確保 Android 應用通訊安全的重要步驟。透過本文的指引,您可以:
- 理解 SSL Pinning 的原理與重要性
- 正確配置自定義信任鏈
- 實作公鑰 Pinning(推薦方式)
- 處理憑證更新與維護問題
- 遵循安全最佳實踐
下一步建議:
- 在測試環境驗證 Pinning 配置
- 實施憑證監控與告警機制
- 準備憑證輪換計劃
- 定期進行安全審計