SSL Pinning
1 HTTPS协议流程
参考:
https://segmentfault.com/a/1190000009002353?sort=newest
https://zhuanlan.zhihu.com/p/353571366
https://juejin.cn/post/6863295544828444686
HTTPS=HTTP+TLS,其它的协议也类似,如FTPS=FTP+TLS
1) ClientHello
- Client 首先发送本地的 TLS 版本、支持的加密算法套件,并且生成一个随机数 R1 。
2)Server Hello
- Server 端确认 TLS 版本号。从 Client 端支持的加密套件中选取一个,并生成一个随机数 R2 一起发送给 Client。
- Server 向 Client 发送自己的CA证书(包含公钥、证书签名)。
3)证书校验
- Client 判断证书签名与CA证书是否合法有效
- Client 生成随机数pre-master secret,并使用Server发过来的公钥对pre-master secret进行加密,将加密后的pre-master secret送给Server。这一步结束后,Client 与 Server 就都有 R1、R2、pre-master secret 了,两端便可以使用这 3 个随机数独立生成 对称会话密钥了,避免了对称密钥的传输,同时可以 根据会话密钥生成 6 个密钥(P1~P6) 用作后续身份验证
Client端和Server端,最终都会用相同的算法将pre-master secret(预主密钥)转换成master secret(主密钥),通过主密钥可以生成session key。两者后续的通信交互数据,将通过session key进行加密。
参考:https://www.laoqingcai.com/tls1.2-premasterkey/
4)Client 握手结束通知
- Client 使用 P1 将之前的握手信息的 hash 值加密并发送给 Server
- Client 发送握手结束消息
5)Server 握手结束通知
- Server 计算之前的握手信息的 hash 值,并与 P1 解密客户端发送的握手信息的 hash 对比校验
- 验证通过后,使用 P2 将之前的握手信息的 hash 值加密并发送给 Client
6)Client 开始HTTPS通讯
- Client 计算之前的握手信息的 hash 值,并与 P2 解密 Server 发送的握手信息的 hash 对比校验
- 验证通过后,开始发起 HTTPS 请求。
两者后续的通信交互数据,将通过session key进行加密。所以中间人即使截获数据,也无法解析。
2 证书相关
证书文件
证书=公钥+(公钥+元信息)的签名
其中的元信息包括:
- Subject(主体信息): - Common Name(CN)通用名称- SAN- Organization- Organization Unit(OU)- Country- State- City- Address- Postal code
- Issuer(签发者信息): - Common Name(CN)通用名称- Organization- Organization Unit(OU)- Country- State- City- Address- Postal code
- Validity(有效期): - Not Before(签发日期)- Not After(过期时间)
- Signature Algorithm
- Serial Number
- Version
- Extensions(扩展信息):只在证书版本2、3中才有
因此,证书的结构大致如下:
CA
签名 = 计算摘要 + 对摘要值私钥加密
CA:Certificate Authority,专门用自己的私钥 给别人进行签名的机构
签发证书的过程
注意,计算签名时,是对整个证书文件计算签名,也就是对【元信息+公钥】计算签名,而不只是对公钥计算签名。
(参考:https://blog.csdn.net/bluishglc/article/details/123617558)
证书的验证过程
关键过程:用信任CA库里CA证书(公钥),验证网站的证书文件里的签名
- 在TLS握手的过程中,客户端得到了网站的证书
- 客户端打开证书,查看是哪个CA签名的这个证书
- 在自己信任的CA库中,找相应CA的证书(包含CA的公钥),
- 用CA证书里面的公钥解密网站证书上的签名,取出网站证书的摘要,然后用同样的算法(比如sha256)算出网站证书的摘要,如果摘要和签名中的摘要对的上,说明这个证书是合法的,且没被人篡改过
- 读出里面的CN,对于网站的证书,里面一般包含的是域名
- 检查里面的域名和自己访问网站的域名对不对的上,对的上,就说明这个证书确实是颁发给这个网站的
- 到此为止检查通过
证书链的验证
参考:
https://www.jianshu.com/p/46e48bc517d0
https://www.cnblogs.com/xiaxveliang/p/13183175.html
我们使用End-user Certificates来确保加密传输数据的公钥(public key)不被篡改,而又如何确保end-user certificates的合法性呢?
这个认证过程跟公钥的认证过程类似,首先获取颁布end-user certificates的CA的证书,然后验证end-user certificates的signature。一般来说,root CAs不会直接颁布end-user certificates的,而是授权给多个二级CA,而二级CA又可以授权给多个三级CA,这些中间的CA就是Intermediates CAs,它们才会颁布end-user certificates。
但是Intermediates Certificates的可靠性又如何保证呢?这就是涉及到证书链,Certificate Chain ,链式向上验证证书,直到Root Certificates,如下图:
中间CA的证书怎么获取?
以百度的TLS证书进行举例,百度服务器证书 签发者公钥(中间机构公钥)通过下图中的URI获取:
3 SSL Pinning
参考:
https://shunix.com/ssl-pinning/
https://zhuanlan.zhihu.com/p/58204817
3.1 原理
默认情况下,只要网站证书的Root CA,属于系统信任的Root CA集合(例如,安卓中系统默认信任 /system/etc/security/ 中CA证书对应的CA)。
(1)情况A:
某个系统信任的Root CA,授权给了可靠的Intermediate CA 1,Intermediate CA 1给www.example.com颁发了一个合法的证书1;
同时该Root CA也授权给了不可靠的Intermediate CA 2(不可靠的原因可能是私钥被泄露),Intermediate CA 2给 www.example.com颁发了一个证书2。
这时候我们希望只信任证书1而不信任证书2,否则一些中间人拿到了证书2,就可以伪装成合法的www.example.com。
这通过修改信任CA集合是较难实现的,因为两个证书的根信任锚是相同的Root CA。当然,可以从信任集中删除Root CA,再添加Intermediate CA 1而不添加Intermediate CA 2。但这意味着我们需要移除Root CA。通常,一个Root CA会作为成千上万个证书的根信任锚,移除Root CA可能引发过大的影响。
(2)情况B:
系统的可信CA集合被篡改。例如,安卓系统在被Root的情况下,用户可以修改系统信任证书(方法例如:https://github.com/doug-leith/cydia)。
这种情况下,app可能需要只信任特定的某个(某些)证书。
原理:
可以采用证书固定。只有当网站的证书链中,至少有一个节点的证书全部内容/证书公钥,跟客户端预埋的证书的内容相匹配,我们的客户端才信任此证书链。
证书固定 与 限制可信CA 的关系
如果把某个Root CA的证书固定起来,那就相当于设置该Root CA为唯一可信的Root CA。
被固定的证书可以是(一般是)某个中间CA的证书。这样,不再是所有以trusted Root CA为根的证书链都仍旧可信了。只有子节点包含该中间CA的证书链才可信。
被固定的证书的Root CA可以不在系统trusted Root CA集合中。
3.2 实现方案
具体实现技术上,SSL Pinning可以分为Certificate Pinning(证书固定)和Public Key Pinning(公钥固定)
3.2.1 证书固定
把证书文件打包进安装包,将app设置为仅接受指定的内置证书,而不接受操作系统内置的CA根证书对应的任何证书。
3.2.2 公钥固定
提取证书中的公钥并内置到App中,通过与服务器对比公钥值,来验证连接的合法性。我们在申请证书时,公钥在证书的续期前后可以保持不变,所以可以解决证书有效期问题。
3.3 实例
3.3.1 证书固定实例:基于TrustManagerFactory
// kotlin语法// 加载证书文件val cf: CertificateFactory = CertificateFactory.getInstance("X.509")val caInput: InputStream =BufferedInputStream(FileInputStream("load-der.crt"))// 使用CertificateFactory生成一个X509Certificate的实例val ca: X509Certificate = caInput.use{
cf.generateCertificate(it)as X509Certificate
}
System.out.println("ca="+ ca.subjectDN)// 创建一个KeyStore实例,并把前边的X509Certificate实例加进去,并起一个别名"ca"val keyStoreType = KeyStore.getDefaultType()val keyStore = KeyStore.getInstance(keyStoreType).apply{load(null,null)setCertificateEntry("ca", ca)}// 创建一个TrustManagerFactory实例,并且使用前边的KeyStore实例进行初始化val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply{init(keyStore)}// 创建一个SSLContext实例,并且使用前面的TrustManagerFactory实例的trustManagers进行初始化val context: SSLContext = SSLContext.getInstance("TLS").apply{init(null, tmf.trustManagers,null)}// 创建HttpsURLConnection实例urlConnectionval url =URL("https://certs.cac.washington.edu/CAtest/")val urlConnection = url.openConnection()as HttpsURLConnection
// 将SSLContext实例context的socketFactory属性,赋值给urlConnection
urlConnection.sslSocketFactory = context.socketFactory
val inputStream: InputStream = urlConnection.inputStream
copyInputStreamToOutputStream(inputStream, System.out)
3.3.2 证书固定实例:基于NSC配置文件
需要在Manifest文件的android:networkSecurityConfig属性加上对应的配置内容,示例如下:
<?xml version="1.0" encoding="utf-8"?><network-security-config><!-- Support certificate file, in der or pem format --><domain-config><domainincludeSubdomains="true">example.com</domain><trust-anchors><certificatessrc="@raw/my_ca"/></trust-anchors></domain-config><!-- Support sha256 hash of subject public key --><domain-config><domainincludeSubdomains="true">example.com</domain><pin-setexpiration="2018-01-01"><pindigest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin><!-- backup pin --><pindigest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin></pin-set></domain-config></network-security-config>
关于NSC的详细内容,可以参考论文:
[USENIX Sec’21] Why Eve and Mallory Still Love Android: Revisiting TLS (In)Security in Android Applications
或者直接参考Google的官网文档:
https://developer.android.com/training/articles/security-config
计划后续写一篇博客详细介绍Google Android的NSC。
4 安卓中的SSL Pinning
参考:http://hanpfei.github.io/2018/03/20/android_cert_mgr_and_verify/
SSL Pinning机制中,客户端将特定域名的证书与特定的签发者绑定。即,对某个域名,客户端只承认特定CA为该域名签发的证书,而不承认其它 CA 为该域名签发的证书。
4.1 Android 的根证书管理
AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates 目录下,而在 Android 系统中,则存放在 /system/etc/security/ 目录下:
cacerts_google 目录下的根证书,主要用于 system/update_engine、external/libbrillo 和 system/core/crash_reporter 等模块
cacerts 目录下的根证书则用于所有的应用。cacerts 目录下的根证书,即 Android 系统的根证书库,像下面这样:
它们都是 PEM 格式的 X.509 证书。
Android 系统通过 SystemCertificateSource、DirectoryCertificateSource 和 CertificateSource 等类管理系统根证书库。
- CertificateSource定义了可以对根证书库执行的操作,主要是对根证书的获取和查找: 位于frameworks/base/core/java/android/security/net/config/CertificateSource.java
- DirectoryCertificateSource 类提供证书的创建、获取和查找操作: 位于frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java 获取根证书库的 getCertificates() 操作在第一次被调用时,遍历文件系统,并加载系统所有的根证书文件,并缓存起来,以备后面访问。 根证书的查找操作,主要依据证书文件的文件名进行,证书文件被要求以 [SubjectName 的哈希值].[Index] 的形式命名。
- SystemCertificateSource 类定义了系统根证书库的路径,以及无效一个根证书的机制: 位于frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java Android 系统的根证书位于 /system/etc/security/cacerts/ 目录下。用户可以通过将特定根证书复制到用户配置目录的 cacerts-removed 目录下来无效一个根证书。
4.2 证书链合法性验证
OpenSSLSocketImpl.startHandshake() 通过 NativeCrypto 类的 SSL_do_handshake() 方法执行握手操作:
(NativeCrypto 位于external/conscrypt/src/main/java/org/conscrypt/NativeCrypto.java)
SSL_do_handshake() 方法的第三参数是一个接口:SSLHandshakeCallbacks
SSLHandshakeCallbacks是NativeCrypto 类定义的接口,其中包含一组回调函数;这组回调函数,是SSL_do_handshake() 的参数,在SSL_do_handshake() 中被传入native层
SSLHandshakeCallbacks 中的方法之一是verifyCertificateChain():
/**
* A collection of callbacks from the native OpenSSL code that are
* related to the SSL handshake initiated by SSL_do_handshake.
*/publicinterfaceSSLHandshakeCallbacks{/**
* Verify that we trust the certificate chain is trusted.
*
* @param sslSessionNativePtr pointer to a reference of the SSL_SESSION
* @param certificateChainRefs chain of X.509 certificate references
* @param authMethod auth algorithm name
*
* @throws CertificateException if the certificate is untrusted
*/publicvoidverifyCertificateChain(long sslSessionNativePtr,long[] certificateChainRefs,String authMethod)throwsCertificateException;
verifyCertificateChain()的参数:
- 指向一个sslSession的指针
- X.509 证书链
- 认证算法名称
SSLHandshakeCallbacks中的回调方法的实现在 OpenSSLSocketImpl 。 OpenSSLSocketImpl中,verifyCertificateChain()的实现如下:
@SuppressWarnings("unused")// used by NativeCrypto.SSLHandshakeCallbacks@OverridepublicvoidverifyCertificateChain(long sslSessionNativePtr,long[] certRefs,String authMethod)throwsCertificateException{try{X509TrustManager x509tm = sslParameters.getX509TrustManager();if(x509tm ==null){thrownewCertificateException("No X.509 TrustManager");}if(certRefs ==null|| certRefs.length ==0){thrownewSSLException("Peer sent no certificate");}OpenSSLX509Certificate[] peerCertChain =newOpenSSLX509Certificate[certRefs.length];for(int i =0; i < certRefs.length; i++){
peerCertChain[i]=newOpenSSLX509Certificate(certRefs[i]);}// Used for verifyCertificateChain callback
handshakeSession =newOpenSSLSessionImpl(sslSessionNativePtr,null, peerCertChain,getHostnameOrIP(),getPort(),null);boolean client = sslParameters.getUseClientMode();if(client){Platform.checkServerTrusted(x509tm, peerCertChain, authMethod,this);if(sslParameters.isCTVerificationEnabled(getHostname())){byte[] tlsData =NativeCrypto.SSL_get_signed_cert_timestamp_list(
sslNativePointer);byte[] ocspData =NativeCrypto.SSL_get_ocsp_response(sslNativePointer);CTVerifier ctVerifier = sslParameters.getCTVerifier();CTVerificationResult result =
ctVerifier.verifySignedCertificateTimestamps(peerCertChain, tlsData, ocspData);if(result.getValidSCTs().size()==0){thrownewCertificateException("No valid SCT found");}}}else{String authType = peerCertChain[0].getPublicKey().getAlgorithm();Platform.checkClientTrusted(x509tm, peerCertChain, authType,this);}}catch(CertificateException e){throw e;}catch(Exception e){thrownewCertificateException(e);}finally{// Clear this before notifying handshake completed listeners
handshakeSession =null;}}
这里面,verifyCertificateChain() 从 OpenSSLSocketImpl的 sslParameters 获得 X509TrustManager:
X509TrustManager x509tm = sslParameters.getX509TrustManager();
然后在 Platform.checkServerTrusted() 中执行服务端证书合法有效性的检查:
Platform.checkServerTrusted(x509tm, peerCertChain, authMethod,this);
Platform.checkServerTrusted在com.android.org.conscrypt.Platform类(external/conscrypt/src/compat/java/org/conscrypt/Platform.java):
publicstaticvoidcheckServerTrusted(X509TrustManager tm,X509Certificate[] chain,String authType,OpenSSLSocketImpl socket)throwsCertificateException{if(!checkTrusted("checkServerTrusted", tm, chain, authType,Socket.class, socket)&&!checkTrusted("checkServerTrusted", tm, chain, authType,String.class,
socket.getHandshakeSession().getPeerHost())){
tm.checkServerTrusted(chain, authType);}}
可以看到,Platform.checkServerTrusted()会调用X509TrustManager.checkServerTrusted()来完成检查。
其中的X509TrustManager实例来源于OpenSSLSocketImpl 的sslParameters,如前文所述:
X509TrustManager x509tm = sslParameters.getX509TrustManager();
那OpenSSLSocketImpl 的 sslParameters 又来自于哪里呢?来源于构造函数,例如:
protectedOpenSSLSocketImpl(SSLParametersImpl sslParameters)throwsIOException{this.socket =this;this.peerHostname =null;this.peerPort =-1;this.autoClose =false;this.sslParameters = sslParameters;}
而OpenSSLSocketFactoryImpl类会实例化OpenSSLSocketImpl:
packageorg.conscrypt;importjava.io.IOException;importjava.net.InetAddress;importjava.net.Socket;importjava.net.UnknownHostException;importjava.security.KeyManagementException;publicclassOpenSSLSocketFactoryImplextendsjavax.net.ssl.SSLSocketFactory{privatefinalSSLParametersImpl sslParameters;privatefinalIOException instantiationException;
…………
@OverridepublicSocketcreateSocket()throwsIOException{if(instantiationException !=null){throw instantiationException;}returnnewOpenSSLSocketImpl((SSLParametersImpl) sslParameters.clone());}@OverridepublicSocketcreateSocket(String hostname,int port)throwsIOException,UnknownHostException{returnnewOpenSSLSocketImpl(hostname, port,(SSLParametersImpl) sslParameters.clone());}@OverridepublicSocketcreateSocket(String hostname,int port,InetAddress localHost,int localPort)throwsIOException,UnknownHostException{returnnewOpenSSLSocketImpl(hostname,
port,
localHost,
localPort,(SSLParametersImpl) sslParameters.clone());}@OverridepublicSocketcreateSocket(InetAddress address,int port)throwsIOException{returnnewOpenSSLSocketImpl(address, port,(SSLParametersImpl) sslParameters.clone());}@OverridepublicSocketcreateSocket(InetAddress address,int port,InetAddress localAddress,int localPort)throwsIOException{returnnewOpenSSLSocketImpl(address,
port,
localAddress,
localPort,(SSLParametersImpl) sslParameters.clone());}}
后面的细节暂时略过不看。
总结:
OpenSSLSocketImpl.startHandshake() 和 NativeCrypto.SSL_do_handshake() 执行完整的 SSL/TLS 握手过程。
证书合法性验证是 SSL/TLS 握手的一个重要步骤。该过程通过 native层调用Java 层的回调方法 SSLHandshakeCallbacks.verifyCertificateChain() 来完成。
回调方法的实现在OpenSSLSocketImpl。
OpenSSLSocketImpl.verifyCertificateChain()调用Platform.checkServerTrusted(),调用RootTrustManager.checkServerTrusted() ,调用NetworkSecurityTrustManager.checkServerTrusted() ,将真正根据系统根证书库执行证书合法性验证的 TrustManagerImpl 和 SSL/TLS 握手过程结合起来。
OpenSSLSocketFactoryImpl 将 OpenSSLSocketImpl 和 SSLParametersImpl 粘起来。
SSLParametersImpl 将 OpenSSLSocketImpl 和 RootTrustManager 粘起来。
NetworkSecurityConfig 将 RootTrustManager 和 NetworkSecurityTrustManager
粘起来。NetworkSecurityConfig、NetworkSecurityTrustManager 和
TrustedCertificateStoreAdapter 将 TrustManagerImpl 和管理系统根证书库的
SystemCertificateSource 粘起来。
TrustManagerImpl 是证书合法性验证的核心,它会查找系统根证书库,并验证服务端证书的合法性做。
这个过程的调用栈如下:
com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted()
android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted()
android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted()
android.security.net.config.RootTrustManager.checkServerTrusted()
com.android.org.conscrypt.Platform.checkServerTrusted()
com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain()
com.android.org.conscrypt.NativeCrypto.SSL_do_handshake()
com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake()
com.android.okhttp.Connection.connectTls()
4.3 自定义证书(SSL Pinning)
在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。
要想定制 OpenSSLSocketImpl 的证书验证过程,必然要改变 SSLParametersImpl;要改变 OpenSSLSocketImpl 的 SSLParametersImpl,则必然需要修改 SSLSocketFactory。修改 SSLSocketFactory 常常是一个不错的方法。
两种实现手段:
(1)自己实现 X509TrustManager
像下面这样:
privatefinalclassHelloX509TrustManagerimplementsX509TrustManager{privateX509TrustManager mSystemDefaultTrustManager;privateX509Certificate mCertificate;privateHelloX509TrustManager(){
mCertificate =loadRootCertificate();
mSystemDefaultTrustManager =systemDefaultTrustManager();}privateX509CertificateloadRootCertificate(){String certName ="netease.crt";X509Certificate certificate =null;InputStream certInput =null;try{
certInput =newBufferedInputStream(MainActivity.this.getAssets().open(certName));CertificateFactory certificateFactory =CertificateFactory.getInstance("X.509");
certificate =(X509Certificate) certificateFactory.generateCertPath(certInput).getCertificates().get(0);}catch(IOException e){
e.printStackTrace();}catch(CertificateException e){
e.printStackTrace();}finally{if(certInput !=null){try{
certInput.close();}catch(IOException e){}}}return certificate;}privateX509TrustManagersystemDefaultTrustManager(){try{TrustManagerFactory trustManagerFactory =TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore)null);TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();if(trustManagers.length !=1||!(trustManagers[0]instanceofX509TrustManager)){thrownewIllegalStateException("Unexpected default trust managers:"+Arrays.toString(trustManagers));}return(X509TrustManager) trustManagers[0];}catch(GeneralSecurityException e){thrownewAssertionError();// The system has no TLS. Just give up.}}@OverridepublicvoidcheckClientTrusted(X509Certificate[] chain,String authType)throwsCertificateException{
mSystemDefaultTrustManager.checkClientTrusted(chain, authType);}@OverridepublicvoidcheckServerTrusted(X509Certificate[] chain,String authType)throwsCertificateException{for(X509Certificate certificate : chain){try{
certificate.verify(mCertificate.getPublicKey());return;}catch(NoSuchAlgorithmException e){
e.printStackTrace();}catch(InvalidKeyException e){
e.printStackTrace();}catch(NoSuchProviderException e){
e.printStackTrace();}catch(SignatureException e){
e.printStackTrace();}}
mSystemDefaultTrustManager.checkServerTrusted(chain, authType);}@OverridepublicX509Certificate[]getAcceptedIssuers(){return mSystemDefaultTrustManager.getAcceptedIssuers();}}
(2)仅修改 X509TrustManager 所用的根证书库
privateTrustManager[]createX509TrustManager(){CertificateFactory cf =null;InputStream in =null;TrustManager[] trustManagers =nulltry{
cf =CertificateFactory.getInstance("X.509");
in =getAssets().open("ca.crt");Certificate ca = cf.generateCertificate(in);KeyStore keystore =KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null,null);
keystore.setCertificateEntry("ca", ca);String tmfAlgorithm =TrustManagerFactory.getDefaultAlgorithm();TrustManagerFactory tmf =TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keystore);
trustManagers = tmf.getTrustManagers();}catch(CertificateException e){
e.printStackTrace();}catch(NoSuchAlgorithmException e){
e.printStackTrace();}catch(KeyStoreException e){
e.printStackTrace();}catch(IOException e1){
e1.printStackTrace();}finally{if(in !=null){try{
in.close();}catch(IOException e){
e.printStackTrace();}}}return trustManagers;}
4.4 双向认证
服务端也可能校验客户端的证书(来确保客户端是合法的客户端),这种情况下需要把客户端预存的证书导入中间人抓包工具中。
版权归原作者 Jouzzy 所有, 如有侵权,请联系我们删除。