---- 轉摘自微信讀書團隊文章
iOS 簽名機制挺復雜,各種證書,Provisioning Profile,entitlements,CertificateSigningRequest,p12,AppID,概念一堆,也很容易出錯,本文嘗試從原理出發,一步步推出為什么會有這么多概念,希望能有助于理解 iOS App 簽名的原理和流程。
先來看看蘋果的簽名機制是為了做什么。在 iOS 出來之前,在主流操作系統(Mac/Windows/Linux)上開發和運行軟件是不需要簽名的,軟件隨便從哪里下載都能運行,導致平臺對第三方軟件難以控制,盜版流行。蘋果希望解決這樣的問題,在 iOS 平臺對第三方 APP 有絕對的控制權,一定要保證每一個安裝到 iOS 上的 APP 都是經過蘋果官方允許的,怎樣保證呢?就是通過簽名機制。
通常我們說的簽名就是數字簽名,它是基于非對稱加密算法實現的。對稱加密是通過同一份密鑰加密和解密數據,而非對稱加密則有兩份密鑰,分別是公鑰和私鑰,用公鑰加密的數據,要用私鑰才能解密,用私鑰加密的數據,要用公鑰才能解密。
簡單說一下常用的非對稱加密算法 RSA 的數學原理,理解簡單的數學原理,就可以理解非對稱加密是怎么做到的,為什么會是安全的:
選兩個質數?p?和?q,相乘得出一個大整數n,例如 p=61,q=53,n=pq=3233
選 1-n 間的隨便一個質數?e,例如 e = 17
經過一系列數學公式,算出一個數字?d,滿足:
a. 通過?n?和?e?這兩個數據一組數據進行數學運算后,可以通過 n 和 d 去反解運算,反過來也可以。
b. 如果只知道?n?和?e,要推導出?d,需要知道?p?和?q,也就是要需要把 n 因數分解。
上述的?(n,e)?這兩個數據在一起就是公鑰,(n,d)?這兩個數據就是私鑰,滿足用公鑰加密,私鑰解密,或反過來公鑰加密,私鑰解密,也滿足在只暴露公鑰(只知道?n?和 e)的情況下,要推導出私鑰?(n,d),需要把大整數?n?因數分解。目前因數分解只能靠暴力窮舉,而n數字越大,越難以用窮舉計算出因數?p?和?q,也就越安全,當?n?大到二進制 1024 位或 2048 位時,以目前技術要破解幾乎不可能,所以非常安全。
若對數字?d?是怎樣計算出來的感興趣,可以詳讀這兩篇文章:RSA 算法原理(一)(二)
現在知道了有非對稱加密這東西,那數字簽名是怎么回事呢?
數字簽名的作用是我對某一份數據打個標記,表示我認可了這份數據(簽了個名),然后我發送給其他人,其他人可以知道這份數據是經過我認證的,數據沒有被篡改過。
有了上述非對稱加密算法,就可以實現這個需求:
首先用一種算法,算出原始數據的摘要。需滿足 a.若原始數據有任何變化,計算出來的摘要值都會變化。 b.摘要要夠短。這里最常用的算法是MD5。
生成一份非對稱加密的公鑰和私鑰,私鑰我自己拿著,公鑰公布出去。
對一份數據,算出摘要后,用私鑰加密這個摘要,得到一份加密后的數據,稱為原始數據的簽名。把它跟原始數據一起發送給用戶。
用戶收到數據和簽名后,用公鑰解密得到摘要。同時用戶用同樣的算法計算原始數據的摘要,對比這里計算出來的摘要和用公鑰解密簽名得到的摘要是否相等,若相等則表示這份數據中途沒有被篡改過,因為如果篡改過,摘要會變化。
之所以要有第一步計算摘要,是因為非對稱加密的原理限制可加密的內容不能太大(不能大于上述 n 的位數,也就是一般不能大于 1024 位/ 2048 位),于是若要對任意大的數據簽名,就需要改成對它的特征值簽名,效果是一樣的。
好了,有了非對稱加密的基礎,知道了數字簽名是什么,怎樣可以保證一份數據是經過某個地方認證的,來看看怎樣通過數字簽名的機制保證每一個安裝到 iOS 上的 APP 都是經過蘋果認證允許的。
要實現這個需求很簡單,最直接的方式,蘋果官方生成一對公私鑰,在 iOS 里內置一個公鑰,私鑰由蘋果后臺保存,我們傳 App 上 AppStore 時,蘋果后臺用私鑰對 APP 數據進行簽名,iOS 系統下載這個 APP 后,用公鑰驗證這個簽名,若簽名正確,這個 APP 肯定是由蘋果后臺認證的,并且沒有被修改過,也就達到了蘋果的需求:保證安裝的每一個 APP 都是經過蘋果官方允許的。
如果我們 iOS 設備安裝 APP 只有從 AppStore 下載這一種方式的話,這件事就結束了,沒有任何復雜的東西,只有一個數字簽名,非常簡單地解決問題。
但實際上因為除了從 AppStore 下載,我們還可以有三種方式安裝一個 App:
開發 App 時可以直接把開發中的應用安裝進手機進行調試。
In-House 企業內部分發,可以直接安裝企業證書簽名后的 APP。
AD-Hoc 相當于企業分發的限制版,限制安裝設備數量,較少用。
蘋果要對用這三種方式安裝的 App 進行控制,就有了新的需求,無法像上面這樣簡單了。
我們先來看第一個,開發時安裝APP,它有兩個個需求:
安裝包不需要傳到蘋果服務器,可以直接安裝到手機上。如果你編譯一個 APP 到手機前要先傳到蘋果服務器簽名,這顯然是不能接受的。
蘋果必須對這里的安裝有控制權,包括
a.經過蘋果允許才可以這樣安裝。
b.不能被濫用導致非開發app也能被安裝。
為了實現這些需求,iOS 簽名的復雜度也就開始增加了。
蘋果這里給出的方案是使用了雙層簽名,會比較繞,流程大概是這樣的:
在你的 Mac 開發機器生成一對公私鑰,這里稱為公鑰L,私鑰L。L:Local
蘋果自己有固定的一對公私鑰,跟上面 AppStore 例子一樣,私鑰在蘋果后臺,公鑰在每個 iOS 設備上。這里稱為公鑰A,私鑰A。A:Apple
把公鑰 L 傳到蘋果后臺,用蘋果后臺里的私鑰 A 去簽名公鑰 L。得到一份數據包含了公鑰 L 以及其簽名,把這份數據稱為證書。
在開發時,編譯完一個 APP 后,用本地的私鑰 L 對這個 APP 進行簽名,同時把第三步得到的證書一起打包進 APP 里,安裝到手機上。
在安裝時,iOS 系統取得證書,通過系統內置的公鑰 A,去驗證證書的數字簽名是否正確。
驗證證書后確保了公鑰 L 是蘋果認證過的,再用公鑰 L 去驗證 APP 的簽名,這里就間接驗證了這個 APP 安裝行為是否經過蘋果官方允許。(這里只驗證安裝行為,不驗證APP 是否被改動,因為開發階段 APP 內容總是不斷變化的,蘋果不需要管。)
上述流程只解決了上面第一個需求,也就是需要經過蘋果允許才可以安裝,還未解決第二個避免被濫用的問題。怎么解決呢?蘋果再加了兩個限制,一是限制在蘋果后臺注冊過的設備才可以安裝,二是限制簽名只能針對某一個具體的 APP。
怎么加的?在上述第三步,蘋果用私鑰 A 簽名我們本地公鑰 L 時,實際上除了簽名公鑰 L,還可以加上無限多數據,這些數據都可以保證是經過蘋果官方認證的,不會有被篡改的可能。
可以想到把 允許安裝的設備 ID 列表 和 App對應的 AppID 等數據,都在第三步這里跟公鑰L一起組成證書,再用蘋果私鑰 A 對這個證書簽名。在最后第 5 步驗證時就可以拿到設備 ID 列表,判斷當前設備是否符合要求。根據數字簽名的原理,只要數字簽名通過驗證,第 5 步這里的設備 IDs / AppID / 公鑰 L 就都是經過蘋果認證的,無法被修改,蘋果就可以限制可安裝的設備和 APP,避免濫用。
到這里這個證書已經變得很復雜了,有很多額外信息,實際上除了 設備 ID / AppID,還有其他信息也需要在這里用蘋果簽名,像這個 APP 里 iCloud / push / 后臺運行 等權限蘋果都想控制,蘋果把這些權限開關統一稱為 Entitlements,它也需要通過簽名去授權。
實際上一個“證書”本來就有規定的格式規范,上面我們把各種額外信息塞入證書里是不合適的,于是蘋果另外搞了個東西,叫 Provisioning Profile,一個 Provisioning Profile 里就包含了證書以及上述提到的所有額外信息,以及所有信息的簽名。
所以整個流程稍微變一下,就變成這樣了:
因為步驟有小變動,這里我們不辭啰嗦重新再列一遍整個流程:
在你的 Mac 開發機器生成一對公私鑰,這里稱為公鑰L,私鑰L。L:Local
蘋果自己有固定的一對公私鑰,跟上面 AppStore 例子一樣,私鑰在蘋果后臺,公鑰在每個 iOS 設備上。這里稱為公鑰A,私鑰A。A:Apple
把公鑰 L 傳到蘋果后臺,用蘋果后臺里的私鑰 A 去簽名公鑰 L。得到一份數據包含了公鑰 L 以及其簽名,把這份數據稱為證書。
在蘋果后臺申請 AppID,配置好設備 ID 列表和 APP 可使用的權限,再加上第③步的證書,組成的數據用私鑰 A 簽名,把數據和簽名一起組成一個 Provisioning Profile 文件,下載到本地 Mac 開發機。
在開發時,編譯完一個 APP 后,用本地的私鑰 L 對這個 APP 進行簽名,同時把第④步得到的 Provisioning Profile 文件打包進 APP 里,文件名為?embedded.mobileprovision,把 APP 安裝到手機上。
在安裝時,iOS 系統取得證書,通過系統內置的公鑰 A,去驗證?embedded.mobileprovision?的數字簽名是否正確,里面的證書簽名也會再驗一遍。
確保了?embedded.mobileprovision?里的數據都是蘋果授權以后,就可以取出里面的數據,做各種驗證,包括用公鑰 L 驗證APP簽名,驗證設備 ID 是否在 ID 列表上,AppID 是否對應得上,權限開關是否跟 APP 里的 Entitlements 對應等。
開發者證書從簽名到認證最終蘋果采用的流程大致是這樣,還有一些細節像證書有效期/證書類型等就不細說了。
上面的步驟對應到我們平常具體的操作和概念是這樣的:
第 1 步對應的是 keychain 里的 “從證書頒發機構請求證書”,這里就本地生成了一堆公私鑰,保存的?CertificateSigningRequest?就是公鑰,私鑰保存在本地電腦里。
第 2 步蘋果處理,不用管。
第 3 步對應把?CertificateSigningRequest?傳到蘋果后臺生成證書,并下載到本地。這時本地有兩個證書,一個是第 1 步生成的,一個是這里下載回來的,keychain 會把這兩個證書關聯起來,因為他們公私鑰是對應的,在XCode選擇下載回來的證書時,實際上會找到 keychain 里對應的私鑰去簽名。這里私鑰只有生成它的這臺 Mac 有,如果別的 Mac 也要編譯簽名這個 App 怎么辦?答案是把私鑰導出給其他 Mac 用,在 keychain 里導出私鑰,就會存成?.p12?文件,其他 Mac 打開后就導入了這個私鑰。
第 4 步都是在蘋果網站上操作,配置 AppID / 權限 / 設備等,最后下載 Provisioning Profile 文件。
第 5 步 XCode 會通過第 3 步下載回來的證書(存著公鑰),在本地找到對應的私鑰(第一步生成的),用本地私鑰去簽名 App,并把 Provisioning Profile 文件命名為?embedded.mobileprovision?一起打包進去。這里對 App 的簽名數據保存分兩部分,Mach-O 可執行文件會把簽名直接寫入這個文件里,其他資源文件則會保存在?_CodeSignature?目錄下。
第 6 - 7 步的打包和驗證都是 Xcode 和 iOS 系統自動做的事。
這里再總結一下這些概念:
證書:內容是公鑰或私鑰,由其他機構對其簽名組成的數據包。
Entitlements:包含了 App 權限開關列表。
CertificateSigningRequest:本地公鑰。
p12:本地私鑰,可以導入到其他電腦。
Provisioning Profile:包含了 證書 / Entitlements 等數據,并由蘋果后臺私鑰簽名的數據包。
前面以開發包為例子說了簽名和驗證的流程,另外兩種方式 In-House 企業簽名和 AD-Hoc 流程也是差不多的,只是企業簽名不限制安裝的設備數,另外需要用戶在 iOS 系統設置上手動點擊信任這個企業才能通過驗證。
而 AppStore 的簽名驗證方式有些不一樣,前面我們說到最簡單的簽名方式,蘋果在后臺直接用私鑰簽名 App 就可以了,實際上蘋果確實是這樣做的,如果去下載一個 AppStore 的安裝包,會發現它里面是沒有?embedded.mobileprovision?文件的,也就是它安裝和啟動的流程是不依賴這個文件,驗證流程也就跟上述幾種類型不一樣了。
據猜測,因為上傳到 AppStore 的包蘋果會重新對內容加密,原來的本地私鑰簽名就沒有用了,需要重新簽名,從 AppStore 下載的包蘋果也并不打算控制它的有效期,不需要內置一個?embedded.mobileprovision?去做校驗,直接在蘋果用后臺的私鑰重新簽名,iOS 安裝時用本地公鑰驗證 App 簽名就可以了。
那為什么發布 AppStore 的包還是要跟開發版一樣搞各種證書和 Provisioning Profile?猜測因為蘋果想做統一管理,Provisioning Profile 里包含一些權限控制,AppID 的檢驗等,蘋果不想在上傳 AppStore 包時重新用另一種協議做一遍這些驗證,就不如統一把這部分放在 Provisioning Profile 里,上傳 AppStore 時只要用同樣的流程驗證這個 Provisioning Profile 是否合法就可以了。
所以 App 上傳到 AppStore 后,就跟你的 證書 / Provisioning Profile 都沒有關系了,無論他們是否過期或被廢除,都不會影響 AppStore 上的安裝包。
到這里 iOS 簽名機制的原理和主流程大致說完了,希望能對理解蘋果簽名和排查日常簽名問題有所幫助。
最后這里再提一下我關于簽名流程的一些的疑問。
企業證書簽名因為限制少,在國內被廣泛用于測試和盜版,fir.im / 蒲公英等測試平臺都是通過企業證書分發,國內一些市場像 PP 助手,愛思助手,一部分安裝手段也是通過企業證書重簽名。通過企業證書簽名安裝的 App,啟動時都會驗證證書的有效期,并且不定期請求蘋果服務器看證書是否被吊銷,若已過期或被吊銷,就會無法啟動 App。對于這種助手的盜版安裝手段,蘋果想打擊只能一個個吊銷企業證書,并沒有太好的辦法。
這里我的疑問是,蘋果做了那么多簽名和驗證機制去限制在 iOS 安裝 App,為什么又要出這樣一個限制很少的方式讓盜版鉆空子呢?若真的是企業用途不適合上 AppStore,也完全可以在 AppStore 開辟一個小的私密版塊,還是通過 AppStore 去安裝,就不會有這個問題了。
另一個問題是我們把 App 傳上 AppStore 后,蘋果會對 App 進行加密,導致 App 體積增大不少,這個加密實際上是沒卵用的,只是讓破解的人要多做一個步驟,運行 App 去內存 dump 出可執行文件而已,無論怎樣加密,都可以用這種方式拿出加密前的可執行文件。所以為什么要做這樣的加密呢?想不到有什么好處。
我們看到前面說的簽名流程很繞很復雜,經常出現各種問題,像有 Provisioning Profile 文件但證書又不對,本地有公鑰證書沒對應私鑰等情況,不理解原理的情況下會被繞暈,我的疑問是,這里為什么不能簡化呢?還是以開發證書為例,為什么一定要用本地 Mac 生成的私鑰去簽名?蘋果要的只是本地簽名,私鑰不一定是要本地生成的,蘋果也可以自己生成一對公私鑰給我們,放在 Provisioning Profile 里,我們用里面的私鑰去加密就行了,這樣就不會有?CertificateSigningRequest?和?p12?的概念,跟本地 keychain 沒有關系,不需要關心證書,只要有 Provisioning Profile 就能簽名,流程會減少,易用性會提高很多,同時蘋果想要的控制一點都不會少,也沒有什么安全問題,為什么不這樣設計呢?
能想到的一個原因是 Provisioning Profile 在非 AppStore 安裝時會打包進安裝包,第三方拿到這個 Provisioning Profile 文件就能直接用起來給他自己的 App 簽名了。但這種問題也挺好解決,只需要打包時去掉文件里的私鑰就行了,所以仍不明白為什么這樣設計。