SSL Pinning 與憑證綁定策略

SSL Pinning and Certificate Pinning Strategies

SSL Pinning 概述

SSL Pinning(憑證綁定)是一種安全機制,用於確保應用程式只與預期的伺服器建立安全連線。透過在應用程式中預先嵌入伺服器的憑證或公鑰資訊,可以有效防止中間人攻擊(Man-in-the-Middle, MITM)。

當應用程式進行 HTTPS 連線時,除了驗證憑證鏈的有效性外,還會額外檢查伺服器憑證是否與預先嵌入的資訊相符。如果不符,連線將被拒絕。

為什麼需要憑證綁定

傳統的 SSL/TLS 驗證依賴於憑證授權機構(CA)系統,但這存在以下風險:

  1. CA 被入侵:如果 CA 被攻擊者控制,可能會簽發偽造憑證
  2. 惡意 CA:使用者可能被誘騙安裝惡意根憑證
  3. 企業代理:某些企業環境會安裝自己的 CA 以進行流量檢查
  4. 政府監控:部分國家可能強制安裝監控憑證

SSL Pinning 可以防止這些攻擊,確保應用程式只信任特定的憑證。

Pinning 類型

1. 憑證綁定(Certificate Pinning)

直接綁定完整的伺服器憑證。

優點:實作簡單,安全性最高 缺點:憑證更新時需要同步更新應用程式

2. 公鑰綁定(Public Key Pinning)

只綁定憑證中的公鑰部分。

優點:憑證更新時若保持相同公鑰,無需更新應用程式 缺點:需要從憑證中提取公鑰

3. SPKI 綁定(Subject Public Key Info)

綁定公鑰資訊的雜湊值,這是目前最推薦的方式。

1
2
3
4
5
6
# 提取憑證的 SPKI 雜湊值
openssl s_client -connect example.com:443 | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
openssl enc -base64

Android 實作範例

使用 Network Security Config(Android 7.0+)

res/xml/network_security_config.xml 中配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2025-01-01">
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <!-- 備用 pin,用於憑證輪換 -->
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

AndroidManifest.xml 中引用:

1
2
3
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

使用 OkHttp

1
2
3
4
5
6
7
8
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

iOS 實作範例

使用 URLSession

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PinningDelegate: NSObject, URLSessionDelegate {
    let pinnedPublicKeyHash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        guard let serverTrust = challenge.protectionSpace.serverTrust,
              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // 提取公鑰並計算雜湊
        let publicKey = SecCertificateCopyKey(certificate)
        let publicKeyData = SecKeyCopyExternalRepresentation(publicKey!, nil)! as Data
        let hash = sha256(data: publicKeyData).base64EncodedString()

        if hash == pinnedPublicKeyHash {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

使用 Alamofire

1
2
3
4
5
6
7
let evaluators: [String: ServerTrustEvaluating] = [
    "api.example.com": PublicKeysTrustEvaluator()
]

let manager = Session(
    serverTrustManager: ServerTrustManager(evaluators: evaluators)
)

伺服器端 HPKP(已廢棄)

HTTP Public Key Pinning(HPKP)曾是一種伺服器端的 Pinning 機制,透過 HTTP 標頭告知瀏覽器應該信任的公鑰。

1
2
3
4
5
Public-Key-Pins:
    pin-sha256="base64+primary==";
    pin-sha256="base64+backup==";
    max-age=5184000;
    includeSubDomains

為何被廢棄

  • 配置錯誤可能導致網站完全無法訪問
  • 可能被攻擊者濫用進行「HPKP 勒索攻擊」
  • 憑證輪換管理複雜

Chrome 從版本 72 開始移除 HPKP 支援。建議改用 Certificate Transparency(CT)機制。

更新與輪換策略

憑證輪換最佳實踐

  1. 始終配置備用 Pin:至少配置兩個 Pin,包含當前和備用憑證
  2. 提前規劃:在憑證到期前 30-60 天開始輪換程序
  3. 階段性部署
    • 第一階段:新增新憑證 Pin 到應用程式
    • 第二階段:等待應用程式更新普及
    • 第三階段:伺服器切換到新憑證
    • 第四階段:移除舊憑證 Pin

版本控制策略

1
2
3
4
5
// 根據應用程式版本動態載入 Pin
val pins = when {
    BuildConfig.VERSION_CODE >= 100 -> listOf(PIN_V2, PIN_V3)
    else -> listOf(PIN_V1, PIN_V2)
}

繞過 SSL Pinning(測試用途)

在開發和安全測試時,可能需要暫時繞過 SSL Pinning。

Frida Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// frida -U -f com.example.app -l bypass.js
Java.perform(function() {
    var TrustManager = Java.use('javax.net.ssl.X509TrustManager');
    var SSLContext = Java.use('javax.net.ssl.SSLContext');

    var TrustManagerImpl = Java.registerClass({
        name: 'com.example.TrustManager',
        implements: [TrustManager],
        methods: {
            checkClientTrusted: function(chain, authType) {},
            checkServerTrusted: function(chain, authType) {},
            getAcceptedIssuers: function() { return []; }
        }
    });

    var TrustManagers = [TrustManagerImpl.$new()];
    var sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, TrustManagers, null);
});

使用 objection

1
2
3
objection -g com.example.app explore
# 進入後執行
android sslpinning disable

注意:這些技術僅供合法的安全測試使用,未經授權的使用可能違法。

最佳實踐

  1. 選擇正確的 Pinning 層級:優先使用 SPKI Pinning,平衡安全性與維護性
  2. 實作備用機制:配置多個 Pin 以支援憑證輪換
  3. 設定合理的過期時間:使用 expiration 屬性避免 Pin 過期導致應用程式無法使用
  4. 監控與告警:實作 Pin 驗證失敗的日誌和告警機制
  5. 優雅降級:考慮在 Pin 驗證失敗時的使用者體驗
  6. 定期審查:定期檢查 Pin 配置是否需要更新
  7. 測試環境隔離:開發環境可使用不同的配置或禁用 Pinning
1
2
3
4
5
6
7
8
// 根據環境切換配置
val client = if (BuildConfig.DEBUG) {
    OkHttpClient.Builder().build()
} else {
    OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build()
}

參考資料

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy