在移动应用SSL证书绑定(Certificate  Pinning)是一种通过将服务器特定的证书或公钥“固定”在客户端应用中,以防范TLS中间人攻击的安全机制。

其核心原理是:应用在建立安全连接时,会校验服务器证书是否与预先置入应用内部的“指纹”(如公钥哈希值)相匹配,而不完全依赖操作系统的根证书信任链。

一、平台实现方案对比

下表对比了Android与iOS平台的主要实现方式:

特性 Android  (推荐) iOS  (推荐)

核心方法 Network  Security  Configuration  (NSC) 自定义  URLSessionDelegate

推荐版本 Android  7.0  (API  24)  及以上 iOS  所有主流版本

实现方式 声明式XML配置文件 编程式实现代理方法

优点 官方支持,配置简单,与代码解耦 灵活度高,可动态处理,控制精细

关键步骤 1.  生成公钥哈希

2.  配置XML文件

3.  在清单中引用 .  实现代理方法

2.  提取并比对服务器公钥哈希

3.  创建使用该代理的Session

备份策略 在<pin-set>中配置多个哈希值 在代码数组中存储多个哈希值

二、各平台实现详解

Android  实现  (使用Network  Security  Configuration)

这是Google官方推荐的方法,通过配置文件实现,无需修改核心代码。

生成公钥哈希

你需要提取服务器证书的公钥并计算其SHA-256哈希(Base64编码)。可以使用OpenSSL命令:

bash

openssl  x509  -in  server.crt  -pubkey  -noout  |  openssl  rsa  -pubin  -outform  der  |  openssl  dgst  -sha256  -binary  |  openssl  enc  -base64

请为主证书和备用证书各生成一个哈希。

创建配置文件

在res/xml/目录下创建network_security_config.xml文件,并配置如下:

xml

<?xml  version="1.0"  encoding="utf-8"?>

<network-security-config>

        <domain-config>

                <!--  指定要固定的域名,includeSubdomains表示是否包含子域名  -->

                <domain  includeSubdomains="true">your-api.example.com</domain>

                <pin-set  expiration="2026-12-31">  <!--  设置过期时间作为提醒  -->

                        <!--  填入主公钥哈希  -->

                        <pin  digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>

                        <!--  填入备用公钥哈希  -->

                        <pin  digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyudPR3wYQlq1F0dN3xQkGQ=</pin>

                </pin-set>

        </domain-config>

</network-security-config>

在应用中启用配置

在AndroidManifest.xml文件的<application>标签中引用此配置:

xml

<application

        android:networkSecurityConfig="@xml/network_security_config"

        ...  >

        ...

</application>

iOS  实现  (使用URLSessionDelegate)

在iOS上,需要通过实现URLSessionDelegate协议中的方法来手动验证。

实现证书验证代理

创建一个遵守URLSessionDelegate协议的类,在urlSession(_:didReceive:completionHandler:)方法中验证公钥。

swift

import  Foundation

import  Security

class  PinningDelegate:  NSObject,  URLSessionDelegate  {

          //  预置正确的公钥哈希值  (Base64编码的SHA-256)

        let  pinnedPublicKeyHashes:  [String]  =  [

                "7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=",

                "fwza0LRMXouZHRC8Ei+4PyudPR3wYQlq1F0dN3xQkGQ="

        ]

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

                                //  确保是服务器信任认证

                guard  challenge.protectionSpace.authenticationMethod  ==  NSURLAuthenticationMethodServerTrust,

                            let  serverTrust  =  challenge.protectionSpace.serverTrust  else  {

                        completionHandler(.cancelAuthenticationChallenge,  nil)

                        return

                }

                                //  评估服务器信任状

                var  secResult  =  SecTrustResultType.invalid

                SecTrustEvaluate(serverTrust,  &secResult)

                

                if  secResult  ==  .proceed  ||  secResult  ==  .unspecified  {

                        //  提取服务器证书链中的公钥并计算哈希

                        if  let  serverPublicKey  =  SecTrustCopyPublicKey(serverTrust)  {

                                var  error:  Unmanaged<CFError>?

                                if  let  publicKeyData  =  SecKeyCopyExternalRepresentation(serverPublicKey,  &error)  as  Data?  {

                                        let  publicKeyHash  =  sha256(data:  publicKeyData).base64EncodedString()

                                        //  比对哈希

                                        if  pinnedPublicKeyHashes.contains(publicKeyHash)  {

                                                completionHandler(.useCredential,  URLCredential(trust:  serverTrust))

                                                return

                                        }

                                }

                        }

                }

                //  验证失败,拒绝连接

                completionHandler(.cancelAuthenticationChallenge,  nil)

        }

                private  func  sha256(data:  Data)  ->  Data  {

                var  hash  =  [UInt8](repeating:  0,  count:  Int(CC_SHA256_DIGEST_LENGTH))

                data.withUnsafeBytes  {  _  =  CC_SHA256($0.baseAddress,  CC_LONG(data.count),  &hash)  }

                return  Data(hash)

        }

}

使用自定义代理发起请求

swift

let  pinningDelegate  =  PinningDelegate()

let  session  =  URLSession(configuration:  .default,  delegate:  pinningDelegate,  delegateQueue:  nil)

//  使用这个session发起的请求都会进行证书绑定校验

let  task  =  session.dataTask(with:  URL(string:  "https://your-api.example.com")!)  {  data,  response,  error  in

        //  处理响应

}

task.resume()

其他平台参考

OpenHarmony:从API  12开始,@ohos.net.http模块的httpRequest接口支持certificatePinning属性,可直接配置公钥哈希值。

跨平台框架(如Flutter):通常依赖第三方插件(如dio的dio_certificate_pinning插件)或通过编写原生平台代码桥接来实现。

三、关键实践与注意事项

固定公钥而非SSL证书:公钥通常比证书更稳定。固定证书(尤其是叶证书)可能导致证书正常轮换时应用无法连接。

必须设置备份密钥:始终固定至少两个密钥。一个当前使用,一个备用。这样可以在不紧急更新应用的情况下轮换服务器密钥。

避免硬编码与安全存储:不要将哈希值以明文形式硬编码在代码中。可考虑进行代码混淆,或从安全的远程配置服务在首次启动时获取(这本身需要一个安全的引导通道)。

制定明确的更新流程:在服务器证书或密钥需要变更前,应先将新哈希值集成到应用新版本中,并确保足够多的用户升级后,再在服务器端进行操作。

实施前请谨慎评估:根据OWASP的建议,由于可能引发可用性风险,在非必要情况下应避免实施证书绑定。仅在完全控制客户端和服务器,且能安全、及时地更新密钥时才考虑使用。

注:如果你在开发中遇到某个具体网络库(如OkHttp、Alamofire)的集成问题,或需要生成特定SSL证书公钥哈希的帮助,可以提出更具体的问题。