背景
需求背景:
- 最近公司的產(chǎn)品涉及App 與硬件端的交互豪筝,App與云端的交互瑞你,硬件端與云端的數(shù)據(jù)交換, 公司需要保障每一端的身份得到信任氯哮,于是引入PKI體系,從外部尋找了對(duì)應(yīng)的PKI供應(yīng)商負(fù)責(zé)協(xié)助搭建與提供PKI相關(guān)服務(wù)商佛;
- 當(dāng)硬件端變成server角色時(shí)喉钢,需要對(duì)請(qǐng)求的客戶端client進(jìn)行身份認(rèn)證。 如果僅僅只認(rèn)證server的身份良姆,我們只用單項(xiàng)認(rèn)證就可以了肠虽。 但是如果要對(duì)client也進(jìn)行身份認(rèn)證,那么就需要用到雙向認(rèn)證的相關(guān)理論和技術(shù)歇盼。
- 要保障各個(gè)端的通信安全舔痕,需要保障在各個(gè)端通信時(shí)數(shù)據(jù)是被加密的,并且采用可信的證書簽名數(shù)據(jù)進(jìn)而交換豹缀;
基礎(chǔ)理論
做的過程中伯复,涉及到一些技術(shù)的名詞,先做一些解釋和理解邢笙, 包括PKI啸如,密碼學(xué)與加解密 (公私鑰,證書鏈氮惯,CA), 證書格式叮雳, HTTPS, 雙向認(rèn)證以及單項(xiàng)認(rèn)證想暗, 抓包分析;
PKI
A public key infrastructure (PKI) is a set of roles, policies, hardware, software and procedures needed to create, manage, distribute, use, store and revoke digital certificates and manage public-key encryption.
PKI的解釋可以參見 百度百科PKI 帘不,或者 維基百科PKI 说莫。 它稱作公鑰基礎(chǔ)設(shè)施。本質(zhì)上它是一種方式方法寞焙,用來管理與實(shí)現(xiàn)數(shù)據(jù)的加密交換储狭,證書管理等,是一套加解密安全保障機(jī)制捣郊;
證書
首先談一下證書辽狈,CA這些。 因?yàn)檫@些內(nèi)容和密碼學(xué)密切相關(guān)呛牲,可以先初步對(duì)現(xiàn)代密碼學(xué)的一些基礎(chǔ)理論和技術(shù)進(jìn)行了解與熟悉刮萌。
加解密與簽名:
- 在現(xiàn)在的數(shù)據(jù)安全中,涉及數(shù)據(jù)的加解密娘扩,一般有對(duì)稱加解密着茸,非對(duì)稱加解密;
- 當(dāng)我們對(duì)數(shù)據(jù)進(jìn)行散列得到摘要畜侦,在用私鑰進(jìn)行加密元扔,就得到數(shù)據(jù)的簽名;
至于具體的加解密過程和簽名算法之類旋膳,可以自行網(wǎng)上搜索了解;
那么途事,證書是什么验懊? 它從何而來?它又有什么作用呢尸变?
證書生成:
其實(shí)這里提到的是CA證書(Certificate Authority Certificate)义图,CA是Certificate Authority的縮寫,也叫“證書授權(quán)中心”召烂。CA證書其實(shí)本質(zhì)是一段明文數(shù)據(jù)和加密數(shù)據(jù)的組合碱工。 CA證書可采用openssl生成;
openssl生成證書的過程可參考:
通過openssl生成私鑰: openssl genrsa -out server.key 1024
根據(jù)私鑰生成證書申請(qǐng)文件csr : openssl req -new -key server.key -out server.csr
使用私鑰對(duì)證書申請(qǐng)進(jìn)行簽名從而生成證書: openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650
這樣就生成了有效期為:10年的證書文件
認(rèn)識(shí)證書
x509 證書一般會(huì)用到三類文件奏夫,key怕篷,csr,crt酗昼。Key是私用密鑰廊谓,openssl格式,通常是rsa算法麻削。 csr是證書請(qǐng)求文件蒸痹,用于申請(qǐng)證書春弥。在制作csr文件的時(shí)候,必須使用自己的私鑰來簽署申請(qǐng)叠荠,還可以設(shè)定一個(gè)密鑰匿沛。crt是CA認(rèn)證后的證書文件(windows下面的csr,其實(shí)是crt)榛鼎,簽署人用自己的key給你簽署的憑證俺祠。
數(shù)字證書的內(nèi)容(CA證書內(nèi)容)
X.509是常見通用的證書格式。所有的證書都符合為Public Key Infrastructure (PKI) 制定的 ITU-T X509 國際標(biāo)準(zhǔn)借帘。
X.509 是比較流行的 SSL 數(shù)字證書標(biāo)準(zhǔn)蜘渣,包含(但不限于)以下的字段:
字段 | 值說明 |
---|---|
對(duì)象名稱(Subject Name) | 用于識(shí)別該數(shù)字證書的信息 |
共有名稱(Common Name) | 對(duì)于客戶證書,通常是相應(yīng)的域名 |
證書頒發(fā)者(Issuer Name) | 發(fā)布并簽署該證書的實(shí)體的信息 |
簽名算法(Signature Algorithm) | 簽名所使用的算法 |
序列號(hào)(Serial Number) | 數(shù)字證書機(jī)構(gòu)(Certificate Authority肺然, CA)給證書的唯一整數(shù)蔫缸,一個(gè)數(shù)字證書一個(gè)序列號(hào) |
生效期(Not Valid Before ) | 無 |
失效期(Not Valid After) | 無 |
公鑰(Public Key) | 可公開的密鑰 |
簽名(Signature) | 通過簽名算法計(jì)算證書內(nèi)容后得到的數(shù)據(jù),用于驗(yàn)證證書是否被篡改 |
指紋(fingerPrint) | 證書的ID |
那么 际起,CA證書一般是什么樣的格式存在呢拾碌?
PKCS#7 常用的后綴是: .P7B .P7C .SPC ;
PKCS#12 常用的后綴有: .P12 .PFX (iOS都會(huì)知道開發(fā)者證書導(dǎo)入的時(shí)候用到了 .P12文件街望,)
X.509 DER 編碼(ASCII)的后綴是: .DER .CER .CRT 校翔,der,cer文件一般是二進(jìn)制格式的,只放證書灾前,不含私鑰 (在iOS中一般是這種.der, .cer格式)防症;
.cer/.crt是用于存放證書,它是2進(jìn)制形式存放的哎甲,不含私鑰(crt文件可能是二進(jìn)制的蔫敲,也可能是文本格式的,應(yīng)該以文本格式居多炭玫,功能同der/cer)奈嘿;
X.509 PAM 編碼(Base64)的后綴是: .PEM .CER .CRT (.pem 跟crt/cer的區(qū)別是它以 Ascii來 表示,pem文件一般是文本格式的吞加,可以放證書或者私鑰裙犹,或者兩者都有,pem如果只含私鑰的話衔憨,一般用.key擴(kuò)展名叶圃,而且可以有密碼保護(hù))
pfx/p12用于存放個(gè)人證書/私鑰,他通常包含保護(hù)密碼巫财,2進(jìn)制方式(pfx,p12文件是二進(jìn)制格式盗似,同時(shí)含私鑰和證書,通常有保護(hù)密碼)
p10是證書請(qǐng)求平项、p7r是CA對(duì)證書請(qǐng)求的回復(fù)赫舒,只用于導(dǎo)入悍及、p7b以樹狀展示證書鏈(certificate chain),同時(shí)也支持單個(gè)證書接癌,不含私鑰心赶。
怎么判斷是文本格式還是二進(jìn)制?
用記事本打開缺猛,如果是規(guī)則的數(shù)字字母缨叫,如
—–BEGIN CERTIFICATE—–
MIIE9jCCA96gAwIBAgIQVXD9d9wgivhJM//a3VIcDjANBgkqhkiG9w0BAQUFADBy
—–END CERTIFICATE—–
就是文本的,上面的BEGIN CERTIFICATE荔燎,說明這是一個(gè)證書耻姥,如果是—–BEGIN RSA PRIVATE KEY—–,說明這是一個(gè)私鑰
自簽證書的生成:
使用openssl來生成一些列的自簽名證書
(1)創(chuàng)建根證書私鑰:
openssl genrsa -out root.key 1024
(2)創(chuàng)建根證書請(qǐng)求文件:
openssl req -new -out root.csr -key root.key
(3)創(chuàng)建根證書
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
HTTPS
為什么要用HTTPS有咨? 解決了什么問題琐簇?
我們知道http的缺點(diǎn):
1. 數(shù)據(jù)明文
2. 不驗(yàn)證通信方的身份
3. 數(shù)據(jù)報(bào)文容易被篡改
4. 容易遭受MITM攻擊
HTTPS 是運(yùn)行在 TLS/SSL 之上的 HTTP,與普通的 HTTP 相比座享,在數(shù)據(jù)傳輸?shù)陌踩陨嫌泻艽蟮奶嵘?br> 為了提高安全性婉商,我們常用的做法是使用對(duì)稱加密的手段加密數(shù)據(jù)≡眩可是只使用對(duì)稱加密的話丈秩,雙方通信的開始總會(huì)以明文的方式傳輸密鑰。那么從一開始這個(gè)密鑰就泄露了淳衙,談不上什么安全蘑秽。所以 TLS/SSL 在握手的階段,結(jié)合非對(duì)稱加密的手段滤祖,保證只有通信雙方才知道對(duì)稱加密的密鑰筷狼。
So, 為什么要用雙向認(rèn)證? 雙向認(rèn)證解決了什么問題匠童?
雙向認(rèn)證,顧名思義塑顺,客戶端和服務(wù)器端都需要驗(yàn)證對(duì)方的身份汤求,在建立https連接的過程中,握手的流程比單向認(rèn)證多了幾步严拒。單向認(rèn)證的過程扬绪,客戶端從服務(wù)器端下載服務(wù)器端公鑰證書進(jìn)行驗(yàn)證,然后建立安全通信通道裤唠。
雙向通信流程挤牛,客戶端除了需要從服務(wù)器端下載服務(wù)器的公鑰證書進(jìn)行驗(yàn)證外,還需要把客戶端的公鑰證書上傳到服務(wù)器端給服務(wù)器端進(jìn)行驗(yàn)證种蘸,等雙方都認(rèn)證通過了墓赴,才開始建立安全通信通道進(jìn)行數(shù)據(jù)傳輸竞膳。
能保證通信的雙方是指定的端,在很多P2P(端對(duì)端)的通信中诫硕,也有很多實(shí)際的應(yīng)用坦辟。
雙向認(rèn)證
什么是單向認(rèn)證?
- 客戶端發(fā)起建立HTTPS連接請(qǐng)求章办,將SSL協(xié)議版本的信息發(fā)送給服務(wù)器端锉走;
- 服務(wù)器端將本機(jī)的公鑰證書(server.crt)發(fā)送給客戶端;
- 客戶端讀取公鑰證書(server.crt)藕届,取出了服務(wù)端公鑰挪蹭;
- 客戶端生成一個(gè)隨機(jī)數(shù)(密鑰R),用剛才得到的服務(wù)器公鑰去加密這個(gè)隨機(jī)數(shù)形成密文休偶,發(fā)送給服務(wù)端梁厉;
- 服務(wù)端用自己的私鑰(server.key)去解密這個(gè)密文,得到了密鑰R
- 服務(wù)端和客戶端在后續(xù)通訊過程中就使用這個(gè)密鑰R進(jìn)行通信了椅贱。
什么是雙向認(rèn)證懂算?
- 客戶端發(fā)起建立HTTPS連接請(qǐng)求,將SSL協(xié)議版本的信息發(fā)送給服務(wù)端庇麦;
- 服務(wù)器端將本機(jī)的公鑰證書(server.crt)發(fā)送給客戶端计技;
- 客戶端讀取公鑰證書(server.crt),取出了服務(wù)端公鑰山橄;
- 客戶端將客戶端公鑰證書(client.crt)發(fā)送給服務(wù)器端垮媒;
- 服務(wù)器端解密客戶端公鑰證書,拿到客戶端公鑰航棱;
- 客戶端發(fā)送自己支持的加密方案給服務(wù)器端睡雇;
- 服務(wù)器端根據(jù)自己和客戶端的能力,選擇一個(gè)雙方都能接受的加密方案饮醇,使用客戶端的公鑰加密后發(fā)送給客戶端它抱;
- 客戶端使用自己的私鑰解密加密方案,生成一個(gè)隨機(jī)數(shù)R朴艰,使用服務(wù)器公鑰加密后傳給服務(wù)器端观蓄;
- 服務(wù)端用自己的私鑰去解密這個(gè)密文,得到了密鑰R
- 服務(wù)端和客戶端在后續(xù)通訊過程中就使用這個(gè)密鑰R進(jìn)行通信了祠墅。
可以理解為侮穿,在SSL握手階段,客戶端和服務(wù)端通過非對(duì)稱加密毁嗦,以及證書鏈校驗(yàn)亲茅,協(xié)商并確保雙方得到一個(gè)會(huì)話秘鑰,連接成功后,采用這個(gè)會(huì)話秘鑰對(duì)數(shù)據(jù)進(jìn)行加密傳輸克锣,就可以保障數(shù)據(jù)的安全性茵肃;
證書的校驗(yàn)是如何執(zhí)行的?
首先娶耍,客戶端收到服務(wù)端發(fā)來的證書鏈數(shù)據(jù)免姿,類似下圖:
服務(wù)端證書一般由中間證書簽發(fā),而中間證書由根證書進(jìn)行簽發(fā)榕酒,根證書由CA機(jī)構(gòu)生成胚膊,上一級(jí)的證書是下一級(jí)證書的簽發(fā)者,由此形成的證書樹形結(jié)構(gòu)想鹰,就是證書鏈紊婉。證書鏈校驗(yàn)過程如下:
1.取上級(jí)證書的公鑰,對(duì)下級(jí)證書的簽名進(jìn)行解密得出下級(jí)證書的摘要digest1 辑舷;
2.對(duì)下級(jí)證書進(jìn)行信息摘要digest2穷缤;
3.判斷digest1是否等于digest2讶舰,相等則說明下級(jí)證書校驗(yàn)通過凄鼻;
- 依次對(duì)各個(gè)相鄰級(jí)別證書實(shí)施1~3步驟蕴坪,直到根證書(或者可信任錨點(diǎn)[trusted anchor]);
備注:因?yàn)橄录?jí)證書是上級(jí)證書CA進(jìn)行簽發(fā)頒布的碌廓,上級(jí)CA會(huì)用自己的私鑰传轰,對(duì)簽發(fā)的下級(jí)證書的相關(guān)信息進(jìn)行加密,得到下級(jí)證書的簽名谷婆;
所以上級(jí)證書的公鑰能夠解密下級(jí)證書的簽名慨蛙,也能證明下級(jí)證書的上級(jí)CA 是正確的。同時(shí)根證書或者錨點(diǎn)證書是內(nèi)置在系統(tǒng)中作為可信證書的
另外: 查看證書的數(shù)據(jù)顯示纪挎,還有一個(gè)叫做指紋的期贫,而指紋是證書的唯一值,通常用于在證書庫中查找特定證書异袄。
指紋不是證書的一部分通砍。相反,它是通過獲取整個(gè)證書(包括簽名)的加密哈希來計(jì)算的烤蜕。
不同的加密實(shí)現(xiàn)可能使用不同的散列算法來計(jì)算指紋埠帕,從而為同一證書提供不同的指紋。
(例如玖绿,Windows Crypto API計(jì)算證書的SHA-1哈希以計(jì)算指紋,而OpenSSL可以生成SHA-256或SHA-1哈希叁巨。)因此斑匪,您需要確保使用數(shù)據(jù)庫指紋的客戶端使用相同的API或一致的哈希算法。
iOS中的雙向認(rèn)證:
目前我們iOS的項(xiàng)目是OC語言的項(xiàng)目,項(xiàng)目中的網(wǎng)絡(luò)請(qǐng)求用了著名的第三方開源庫AFNetwoking, 我們知道AFNetworking也是基于蘋果原生網(wǎng)絡(luò)類NSURLSession封裝得到蚀瘸。
目前獲取數(shù)據(jù)的接口API后端環(huán)境 狡蝶,已開啟了雙向認(rèn)證,服務(wù)端雙向認(rèn)證的配置由IT人員完成贮勃。于是需要在AFN請(qǐng)求接口的時(shí)候完成HTTPS雙向認(rèn)證的處理贪惹。
還有一部分也需要注意,App當(dāng)中會(huì)涉及利用WKWebView加載一些H5頁面寂嘉,而前端的網(wǎng)頁部署是同一套環(huán)境的時(shí)候奏瞬,訪問也需要完成HTTPS的認(rèn)證,同時(shí)網(wǎng)頁當(dāng)中也會(huì)通過我們的API接口訪問一些數(shù)據(jù)泉孩,這些也是需要完成HTTPS雙向認(rèn)證硼端。 所以,iOS當(dāng)中會(huì)有至少兩部分的HTTPS認(rèn)證處理寓搬。
實(shí)際處理如下:
- 第一部分珍昨,項(xiàng)目工程ATS的配置
- 第二部分,針對(duì)接口網(wǎng)絡(luò)請(qǐng)求: NSURLSession句喷,NSURLConnection 等的認(rèn)證處理镣典;
- 第三部分,針對(duì)WKWebView加載H5頁面的處理唾琼;
iOS的網(wǎng)絡(luò)請(qǐng)求中兄春,有URL Loading System,是整個(gè)網(wǎng)絡(luò)請(qǐng)求上層的基礎(chǔ):
在認(rèn)證過程中父叙,我們最主要接觸到和處理的是: NSURLSession 神郊、NSURLCredential 、NSURLProtocol
接口網(wǎng)絡(luò)請(qǐng)求的雙向認(rèn)證處理:
- 當(dāng)在iOS中發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求趾唱,如果是HTTPS的域名涌乳,NSURLSession會(huì)觸發(fā)回調(diào)方法,-URLSession:didReceiveChallenge:completionHandler: 回調(diào)中會(huì)收到一個(gè) challenge甜癞,也就是質(zhì)詢夕晓,需要你提供認(rèn)證信息才能完成連接。通過challenge.protectionSpace.authenticationMethod 取得保護(hù)空間protectionSpace要求我們認(rèn)證的方式悠咱;
- 如果這個(gè)值是 NSURLAuthenticationMethodServerTrust 的話蒸辆,代表需要對(duì)服務(wù)端證書的認(rèn)證挑戰(zhàn)進(jìn)行處置,如果這個(gè)值是 NSURLAuthenticationMethodClientCertificate 的話,代表服務(wù)端要求客戶端提供證書接受認(rèn)證挑戰(zhàn)析既;
查看AFN的源碼知道躬贡,AFHTTPSessionManager實(shí)例有個(gè)方法
- setSessionDidReceiveAuthenticationChallengeBlock:
就是用來實(shí)現(xiàn) -URLSession:didReceiveChallenge:completionHandler: 的代理方法的, 所以眼坏,對(duì)于接口的HTTPS雙向認(rèn)證拂玻,我們都可以放在這個(gè)代理方法中去實(shí)現(xiàn), 具體實(shí)現(xiàn)后面再說。
WKWebview加載網(wǎng)頁H5的雙向認(rèn)證處理:
UIWebView UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations (r. 10131336) . You can work around this limitation using an NSURLProtocol subclass, as illustrated by Sample Code 'CustomHTTPProtocol'.
從官方 HTTPS Server Trust Evaluation 的介紹來看檐蚜,對(duì)于webView加載自簽名的HTTPS網(wǎng)站魄懂,不能直接采用NSURLSession的方式處理。根據(jù) iOS使用NSURLProtocol來Hook攔截WKWebview請(qǐng)求 的介紹闯第, 因?yàn)閣ebView的內(nèi)核通信是IPC(進(jìn)程間通信)市栗,和APP是不同的進(jìn)程,不能對(duì)web的請(qǐng)求直接進(jìn)行攔截處理咳短。
WKWebView 在獨(dú)立于 App Process 進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求填帽,請(qǐng)求數(shù)據(jù)不經(jīng)過主進(jìn)程,因此诲泌,在WKWebView 上直接使用 NSURLProtocol 無法攔截請(qǐng)求盲赊。
第一種處理方式: 我們可以使用私有類 WKBrowsingContextController 通過 registerSchemeForCustomProtocol 方法向 WebProcessPool 注冊(cè)全局自定義 scheme 來達(dá)到我們的目的
同時(shí),在讓W(xué)KWebview支持NSURLProtocol 的時(shí)候敷扫,也需要注意一些官方對(duì)于WKWebView的審核規(guī)則哀蘑,避免出現(xiàn)私有API的調(diào)用,具體實(shí)現(xiàn)方式見后面葵第。
因?yàn)橐陨系谝环N處理方式绘迁,經(jīng)驗(yàn)證,避免不了Web當(dāng)中存在的POST請(qǐng)求body數(shù)據(jù)被清空卒密,需要額外處理缀台,同時(shí),處理的方式也比較復(fù)雜哮奇。 但發(fā)現(xiàn)WKWebView已經(jīng)提供了代理方法膛腐,用來處理自定義證書信任策略等,于是采用 第二種處理方式 鼎俘,我們?cè)?/strong>
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler
當(dāng)中去實(shí)現(xiàn)H5的雙向認(rèn)證邏輯
實(shí)踐
編碼
根據(jù)整個(gè)業(yè)務(wù)流程哲身,我們把編碼實(shí)現(xiàn)過程大致分為以下幾步:
- App啟動(dòng)時(shí)候,通過PKI供應(yīng)商的HTTPS通道贸伐,進(jìn)行證書的申請(qǐng)和校驗(yàn)勘天,并將證書保存在App中,并且完成ATS的設(shè)置捉邢;
- 從證書當(dāng)中導(dǎo)出雙向認(rèn)證需要的自簽名的根證書脯丝,以及該移動(dòng)設(shè)備關(guān)聯(lián)的客戶端證書數(shù)據(jù);
- 在App當(dāng)中接口請(qǐng)求的時(shí)候伏伐,通過根證書宠进,以及客戶端證書數(shù)據(jù)完成雙向認(rèn)證;
- 在App當(dāng)中完成加載H5頁面時(shí)藐翎,
攔截并重構(gòu)請(qǐng)求砰苍,完成網(wǎng)頁及網(wǎng)頁請(qǐng)求接口的雙向認(rèn)證潦匈,通過代理實(shí)現(xiàn)雙向認(rèn)證; - 優(yōu)化App應(yīng)用證書的啟動(dòng)過程赚导,以及證書管理的安全等;
證書的申請(qǐng)赤惊,因?yàn)椴捎玫谌降腟DK實(shí)現(xiàn)的吼旧,編碼工作較為簡(jiǎn)單,簡(jiǎn)單調(diào)用SDK的API實(shí)現(xiàn)即可未舟,本質(zhì)上是一個(gè)文件下載的過程圈暗,下載下來的證書數(shù)據(jù)格式為 .pfx / .p12,保存在App的沙河目錄下裕膀,有口令可以對(duì)P12文件數(shù)據(jù)進(jìn)行導(dǎo)出導(dǎo)入,而ATS設(shè)置中员串,需要對(duì)于公司HTTPS域名進(jìn)行例外處理,而ATS不開啟會(huì)觸發(fā)額外的審核昼扛,上架時(shí)候需要說明ATS設(shè)置的緣由
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>xxxx.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
從證書當(dāng)中導(dǎo)出所需數(shù)據(jù)
NSData *PKCS12Data = [NSData dataWithContentsOfFile:localFilePath];
self.pkcs12FileData = PKCS12Data;
if (PKCS12Data) {
SecTrustRef trust = NULL;
if ([self extractServerTrustData:&trust fromPKCS12Data:PKCS12Data]) {
CFIndex certCount;
certCount = SecTrustGetCertificateCount(trust);
if (certCount >= 1) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, certCount - 1);
NSData *data = (__bridge_transfer NSData *)SecCertificateCopyData(certificate);
self.serverRootCerData = data;
}
}
}
// extractServerTrustData當(dāng)中實(shí)現(xiàn)數(shù)據(jù)的獲取
NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:authCode
forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDictionary, &items);
if (securityError == 0) {
CFDictionaryRef trustDataDict = CFArrayGetValueAtIndex(items, 0);
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue(trustDataDict, kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
*outIdentity = (SecIdentityRef)CFDictionaryGetValue(trustDataDict, kSecImportItemIdentity);//客戶端證書憑證數(shù)據(jù)
*certArr = (CFArrayRef)CFDictionaryGetValue(certDataDict, kSecImportItemCertChain);
}
接口進(jìn)行雙向認(rèn)證
// 如果是NSURLSession請(qǐng)求寸齐,代理中進(jìn)行處理, AFN請(qǐng)求中,調(diào)用 [manager setSessionDidReceiveAuthenticationChallengeBlock:]
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
// 對(duì)服務(wù)端證書進(jìn)行認(rèn)證
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
OSStatus err;
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
if (trust == NULL) {
return;
}
// 設(shè)置錨點(diǎn)證書
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
// 證書的數(shù)據(jù)是之前獲取的根證書數(shù)據(jù)
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
if (err == noErr) {
err = SecTrustSetAnchorCertificatesOnly(trust, false);
}
CFErrorRef error = NULL;
if (@available(iOS 12.0, *)) {
__unused bool r = SecTrustEvaluateWithError(trust, &error);
if (error == noErr) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
} else {
SecTrustResultType trustResult = kSecTrustResultInvalid;
err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
if (err == noErr) {
if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
}
}
} else {
// 服務(wù)端接收客戶端證書認(rèn)證
SecIdentityRef identity = NULL;
CFArrayRef certArray = NULL;
if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
// identity 系統(tǒng)使用的證書數(shù)據(jù)身份抄谐,客戶端證書對(duì)應(yīng)的數(shù)據(jù)
// certificates 建議傳nil渺鹦,除非服務(wù)端需要傳遞 intermediate certifate,一般服務(wù)端內(nèi)置了中間證書
// NSURLCredentialPersistenceForSession 對(duì)于網(wǎng)絡(luò)雙向認(rèn)證蛹含,只用填寫這個(gè)值就可以
if (identity) {
customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
}
disposition = NSURLSessionAuthChallengeUseCredential;
}
}
if (completionHandler) {
completionHandler(disposition, customCredential);
}
}
App WKWebView加載 H5
首先需要通過NSURLProtocol 對(duì)webView請(qǐng)求攔截重構(gòu)毅厚,實(shí)現(xiàn)雙向認(rèn)證的處理
// 避免私有API審核被拒
Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if (cls && [cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 只需要匹配https的scheme, 這里不攔截http協(xié)議的請(qǐng)求
[cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
// 注冊(cè)網(wǎng)絡(luò)請(qǐng)求協(xié)議代理類到URL Loading System
[NSURLProtocol registerClass:LLURLProtocol.class];
在 LLURLProtocol的類中,我們把webView的請(qǐng)求進(jìn)行了HOOK浦箱,然后通過 NSURLSession 構(gòu)造了新的請(qǐng)求吸耿。LLURLProtocol是繼承自NSURLProtocol, NSURLProtocol需要實(shí)現(xiàn)幾個(gè)協(xié)議方法酷窥,可以 理解 NSURLProtocol:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;
于是我做了如下的實(shí)現(xiàn):
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
if ([NSURLProtocol propertyForKey:HTTPHandledIdentifier inRequest:request]) {
return NO;
}
// 對(duì)于指定的host咽安,允許hook,則返回YES竖幔,否則NO
return result;
}
// 插入防止重復(fù)請(qǐng)求的標(biāo)志
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:HTTPHandledIdentifier
inRequest:mutableReqeust];
return mutableReqeust;
}
// 啟動(dòng)請(qǐng)求
- (void)startLoading {
self.startDate = [NSDate date];
self.data = [NSMutableData data];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.dataTask = [self.session dataTaskWithRequest:self.request];
[self.dataTask resume];
}
// 請(qǐng)求結(jié)束
- (void)stopLoading {
[self.dataTask cancel];
self.dataTask = nil;
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
}
// 里面的實(shí)現(xiàn)方式和第3步的是一樣的板乙。
}
至此,我們便完成對(duì)于WebView加載自簽名的HTTPS的H5加載了拳氢。
以上第一種實(shí)現(xiàn)方式有bug募逞,采用第二種webView代理的方式直接實(shí)現(xiàn):
// 處理webView雙向認(rèn)證的信任邏輯
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler {
// 只對(duì)API的域名進(jìn)行雙向認(rèn)證處理,其他web頁不處理
NSString *authHost = challenge.protectionSpace.host;
if (![authHost containsString:@"xxx.com"]) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
if (completionHandler) {
completionHandler(disposition, customCredential);
}
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeUseCredential;
NSURLCredential *customCredential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 服務(wù)端認(rèn)證
OSStatus err;
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
// 設(shè)置錨點(diǎn)證書
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverIntermediateCerData)];
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
if (err == noErr) {
err = SecTrustSetAnchorCertificatesOnly(trust, false);
}
if (err != noErr) {
NSLog(@"\nset anchor certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
}
CFErrorRef error = NULL;
if (@available(iOS 12.0, *)) {
__unused bool r = SecTrustEvaluateWithError(trust, &error);
if (error == noErr) {
customCredential = [NSURLCredential credentialForTrust:trust];
} else {
NSLog(@"\nverify server certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
}
} else {
SecTrustResultType trustResult = kSecTrustResultInvalid;
err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
if (err == noErr) {
if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
}
}
} else {
// 客戶端發(fā)送證書認(rèn)證
SecIdentityRef identity = NULL;
CFArrayRef certArray = NULL;
// 從PKCS12文件數(shù)據(jù)導(dǎo)出identity
if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
// identity 系統(tǒng)使用的證書數(shù)據(jù)身份馋评,客戶端證書對(duì)應(yīng)的數(shù)據(jù)
// certificates 建議傳nil放接,除非服務(wù)端需要傳遞 intermediate certifate,一般服務(wù)端內(nèi)置了中間證書
// NSURLCredentialPersistenceForSession 對(duì)于網(wǎng)絡(luò)雙向認(rèn)證留特,只用填寫這個(gè)值就可以
if (identity) {
customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
}
disposition = NSURLSessionAuthChallengeUseCredential;
}
}
if (completionHandler) {
completionHandler(disposition, customCredential);
}
}
調(diào)試
當(dāng)需要對(duì)網(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行分析纠脾,需要和IT人員一起查找并確定網(wǎng)關(guān)玛瘸,或者服務(wù)端代理,配置等問題時(shí)苟蹈,需要有好的工具才可以讓我們事半功倍糊渊。 而在mac上,Apple官方也給出了建議的參考工具慧脱,如 Taking Advantage of Third-Party Network Debugging Tools 所說渺绒,包括其他很多IT人員也會(huì)用,選擇 WireShark 這款應(yīng)用菱鸥。
Wireshark is a free and open source packet analyzer that supports macOS.
至于該如何使用宗兼,我們可以參照網(wǎng)絡(luò)的使用說明,簡(jiǎn)單看一下怎么抓包使用:
打開wireShark應(yīng)用氮采,然后我們連接的網(wǎng)絡(luò)是有線USB 還是無線Wi-Fi殷绍,選擇后就可以進(jìn)入抓包界面。
一般情況下鹊漠,我們需要過濾我們關(guān)心的端口主到,或者協(xié)議,支持篩選:
抓包完成停止后贸呢,就可以 在File - > Export specified Packets 镰烧,當(dāng)中導(dǎo)出當(dāng)前的抓包數(shù)據(jù), 好了楞陷,接下來把抓包數(shù)據(jù)給IT人員一起分析怔鳖,查找問題吧!
總結(jié)
PKI和HTTPS雙向認(rèn)證固蛾, 第一步首先得將需要的服務(wù)端證書结执,以及客戶端證書數(shù)據(jù)打包到App當(dāng)中,至于證書的獲取形式是預(yù)先打包艾凯,還是通過可靠的下載渠道進(jìn)行下載献幔,就跟業(yè)務(wù)的規(guī)劃有關(guān)了;
要完成HTTPS雙向認(rèn)證趾诗,需要分別對(duì)于接口的請(qǐng)求蜡感,NSURLSession的代理進(jìn)行證書數(shù)據(jù)的校驗(yàn),
也需要對(duì)于WKWebView加載的H5做HOOK恃泪,然后轉(zhuǎn)到代理去完成證書的認(rèn)證郑兴;調(diào)試中利用好一些工具比如WireShark,從抓包數(shù)據(jù)可以直接看到SSL握手過程贝乎,以及證書數(shù)據(jù)的傳輸?shù)惹榱膫€(gè)環(huán)節(jié)出現(xiàn)了問題,更有效地排查解決掉問題览效。
-
還有個(gè)問題需要繼續(xù)測(cè)試并觀察:iOS - NSProtocol 攔截 WKWebView POST 請(qǐng)求 body 會(huì)被清空的問題解決
可以棄用攔截WKWebView的方式却舀,在WKWebview的代理當(dāng)中完成授權(quán)認(rèn)證虫几,可以避免這種問題出現(xiàn)
最后:文中的很多內(nèi)容都是自己摸索和不斷理解得到的,如有理解偏差的挽拔,歡迎指正交流辆脸!
參考
iOS 中對(duì) HTTPS 證書鏈的驗(yàn)證
HTTPS雙向認(rèn)證研究
證書的信任鏈校驗(yàn):certificate trust chain
iOS使用NSURLProtocol來Hook攔截WKWebview請(qǐng)求并回放的一種姿勢(shì)
Overriding TLS Chain Validation Correctly
Creating Certificates for TLS Testing
HTTPS Server Trust Evaluation
Taking Advantage of Third-Party Network Debugging Tools
iOS 12、macOS 10.14篱昔、watchOS 5 和 tvOS 12 中可用的受信任根證書列表