在正式解釋Android應用程序簽名過程之前,作為鋪墊,還得先講講最基本的一些概念。
?
非對稱加密算法
?
非對稱加密算法需要兩個密鑰:公開密鑰(簡稱公鑰)和私有密鑰(簡稱私鑰)。公鑰與私鑰是一對,如果用公鑰對數據進行加密,只有用對應的私鑰才能解密;如果用私鑰對數據進行加密,那么只有用對應的公鑰才能解密。因為加密和解密使用的是兩個不同的密鑰,所以這種算法叫作非對稱加密算法。
非對稱加密算法是數字簽名和數字證書的基礎,大家非常熟悉的RSA就是非對稱加密算法的一種實現。
消息摘要算法
消息摘要算法(Message Digest Algorithm)是一種能產生特殊輸出格式的算法,其原理是根據一定的運算規則對原始數據進行某種形式的信息提取,被提取出的信息就被稱作原始數據的消息摘要。著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的變體。
消息摘要的主要特點有:
?
1)無論輸入的消息有多長,計算出來的消息摘要的長度總是固定的。例如應用MD5算法摘要的消息有128個比特位,用SHA-1算法摘要的消息最終有160比特位的輸出。
2)一般來說(不考慮碰撞的情況下),只要輸入的原始數據不同,對其進行摘要以后產生的消息摘要也必不相同,即使原始數據稍有改變,輸出的消息摘要便完全不同。但是,相同的輸入必會產生相同的輸出。
3)具有不可逆性,即只能進行正向的信息摘要,而無法從摘要中恢復出任何的原始消息。
數字簽名和數字證書
其實數字簽名的概念很簡單。大家知道,要確保可靠通信,必須要解決兩個問題:首先,要確定消息的來源確實是其申明的那個人;其次,要保證信息在傳遞的過程中不被第三方篡改,即使被篡改了,也可以發覺出來。
所謂數字簽名,就是為了解決這兩個問題而產生的,它是對前面提到的非對稱加密技術與數字摘要技術的一個具體的應用。
對于消息的發送者來說,先要生成一對公私鑰對,將公鑰給消息的接收者。
如果消息的發送者有一天想給消息接收者發消息,在發送的信息中,除了要包含原始的消息外,還要加上另外一段消息。這段消息通過如下兩步生成:
1)對要發送的原始消息提取消息摘要;
2)對提取的信息摘要用自己的私鑰加密。
通過這兩步得出的消息,就是所謂的原始信息的數字簽名。
而對于信息的接收者來說,他所收到的信息,將包含兩個部分,一是原始的消息內容,二是附加的那段數字簽名。他將通過以下三步來驗證消息的真偽:
1)對原始消息部分提取消息摘要,注意這里使用的消息摘要算法要和發送方使用的一致;
2)對附加上的那段數字簽名,使用預先得到的公鑰解密;
3)比較前兩步所得到的兩段消息是否一致。如果一致,則表明消息確實是期望的發送者發的,且內容沒有被篡改過;相反,如果不一致,則表明傳送的過程中一定出了問題,消息不可信。
通過這種所謂的數字簽名技術,確實可以有效解決可靠通信的問題。如果原始消息在傳送的過程中被篡改了,那么在消息接收者那里,對被篡改的消息提取的摘要肯定和原始的不一樣。并且,由于篡改者沒有消息發送方的私鑰,即使他可以重新算出被篡改消息的摘要,也不能偽造出數字簽名。
所以,綜上所述,數字簽名其實就是只有信息的發送者才能產生的別人無法偽造的一段數字串,這段數字串同時也是對信息的發送者發送信息真實性的一個有效證明。
不知道大家有沒有注意,前面講的這種數字簽名方法,有一個前提,就是消息的接收者必須要事先得到正確的公鑰。如果一開始公鑰就被別人篡改了,那壞人就會被你當成好人,而真正的消息發送者給你發的消息會被你視作無效的。而且,很多時候根本就不具備事先溝通公鑰的信息通道。那么如何保證公鑰的安全可信呢?這就要靠數字證書來解決了。
所謂數字證書,一般包含以下一些內容:
?
- 證書的發布機構(Issuer)
- 證書的有效期(Validity)
- 消息發送方的公鑰
- 證書所有者(Subject)
- 數字簽名所使用的算法
- 數字簽名
?
可以看出,數字證書其實也用到了數字簽名技術。只不過要簽名的內容是消息發送方的公鑰,以及一些其它信息。但與普通數字簽名不同的是,數字證書中簽名者不是隨隨便便一個普通的機構,而是要有一定公信力的機構。這就好像你的大學畢業證書上簽名的一般都是德高望重的校長一樣。一般來說,這些有公信力機構的根證書已經在設備出廠前預先安裝到了你的設備上了。所以,數字證書可以保證數字證書里的公鑰確實是這個證書的所有者的,或者證書可以用來確認對方的身份。數字證書主要是用來解決公鑰的安全發放問題。
綜上所述,總結一下,數字簽名和簽名驗證的大體流程如下圖所示:
?
Android應用程序簽名流程
?
大家知道,Android采用的是開放的生態系統,任何人都可以開發和發布應用程序給別人使用。不像iOS,在沒有被破解的情況下只能通過App Store安裝應用,Android在打開了“Unknow Sources”選項后,可以安裝任何來源的應用程序,可以是第三方市場,可以是自己開發的應用,也可以從論壇下載。
那么問題來了,對于有一些不懷好意的人,完全可以拿到一個原生的應用,然后加入一些惡意的代碼,再發布出去,誘使別人去安裝,達到不可告人的目的。
有沒有什么辦法可以防止應用程序在傳送的過程中被第三方惡意篡改呢?Google因此引入了應用程序簽名機制。
它是如何工作的呢?我們先來看看簽名前后,一個apk文件到底發生了哪些變化。
首先,在沒簽名之前,apk文件內的目錄結構是這樣的:
?
而簽名之后,會變成這樣:
?
可以看到,多出來了一個META-INF目錄。可以肯定的是,簽名的機關就在這個目錄中,里面有三個文件:
?
其實,在Android的源代碼里包含了一個工具,可以對apk文件進行簽名,具體的代碼位置在build\tools\signapk目錄下,通過分析其中的SignApk.java文件,可以大致了解簽名的過程。其流程大致有如下幾步:
1)打開待簽名的apk文件(由于apk其實是一個用zip壓縮的文件,其實就是用zip解壓整個apk文件),逐一遍歷里面的所有條目,如果是目錄就跳過,如果是一個文件,就用SHA1(或者SHA256)消息摘要算法提取出該文件的摘要然后進行BASE64編碼后,作為“SHA1-Digest”屬性的值寫入到MANIFEST.MF文件中的一個塊中。該塊有一個“Name”屬性,其值就是該文件在apk包中的路徑。
2)計算這個MANIFEST.MF文件的整體SHA1值,再經過BASE64編碼后,記錄在CERT.SF主屬性塊(在文件頭上)的“SHA1-Digest-Manifest”屬性值值下。
然后,再逐條計算MANIFEST.MF文件中每一個塊的SHA1,并經過BASE64編碼后,記錄在CERT.SF中的同名塊中,屬性的名字是“SHA1-Digest”。
3)把之前生成的?CERT.SF文件,?用私鑰計算出簽名, 然后將簽名以及包含公鑰信息的數字證書一同寫入??CERT.RSA??中保存。CERT.RSA是一個滿足PKCS7格式的文件,可以通過openssl工具來查看簽名證書的信息。在Ubuntu或者在Windows上使用Cygwin,敲入以下命令:
?
[plain]?view plaincopy
openssl?pkcs7?-inform?DER?-in?CERT.RSA?-noout?-print_certs?–text?? 可以得到如下輸出:
?
下面我們來看看,如果apk文件被篡改后會發生什么。
?
首先,如果你改變了apk包中的任何文件,那么在apk安裝校驗時,改變后的文件摘要信息與MANIFEST.MF的檢驗信息不同,于是驗證失敗,程序就不能成功安裝。
其次,如果你對更改的過的文件相應的算出新的摘要值,然后更改MANIFEST.MF文件里面對應的屬性值,那么必定與CERT.SF文件中算出的摘要值不一樣,照樣驗證失敗。
最后,如果你還不死心,繼續計算MANIFEST.MF的摘要值,相應的更改CERT.SF里面的值,那么數字簽名值必定與CERT.RSA文件中記錄的不一樣,還是失敗。
那么能不能繼續偽造數字簽名呢?不可能,因為沒有數字證書對應的私鑰。
所以,如果要重新打包后的應用程序能再Android設備上安裝,必須對其進行重簽名。
總結
1)Android應用程序簽名只是用來解決發布的應用不被別人篡改的,其并不會對應用程序本身進行加密,這點不同于Windows Phone和iOS。
2)Android并不要求所有應用程序的簽名證書都由可信任CA的根證書簽名,通過這點保證了其生態系統的開放性,所有人都可以用自己生成的證書對應用程序簽名。
3)如果想修改一個已經發布的應用程序,哪怕是修改一張圖片,都必須對其進行重新簽名。但是,簽原始應用的私鑰一般是拿不到的(肯定在原始應用程序開發者的手上,且不可能公布出去),所以只能用另外一組公私鑰對,生成一個新的證書,對重打包的應用進行簽名。所以重打包的apk中所帶證書的公鑰肯定和原始應用不一樣。同時,在手機上如果想安裝一個應用程序,應用程序安裝器會先檢查相同包名的應用是否已經被安裝過,如果已經安裝過,會繼續判斷已經安裝的應用和將要安裝的應用,其所攜帶的數字證書中的公鑰是否一致。如果相同,則繼續安裝;而如果不同,則會提示用戶先卸載前面已安裝的應用。通過這種方式來提示用戶,前后兩個應用是不同開發者簽名的,可能有一個是李鬼。
?
Android應用程序簽名驗證過程分析
? 版權聲明:本文為博主原創文章,未經博主允許不得轉載。
在前面的《Android應用程序簽名過程分析》中,我大致分析了Android應用程序簽名的過程,接下來我將結合源代碼,分析一下Android應用程序在安裝過程中對簽名進行驗證的過程。
我們還是用前面的例子分析,假設簽名后,apk文件中多了一個META-INF目錄,里面有三個文件,分別是MANIFEST.MF、CERT.SF和CERT.RSA:
?
通過前面的分析,我們可以知道,MANIFEST.MF中記錄的是apk中所有文件的摘要值;CERT.SF中記錄的是對MANIFEST.MF的摘要值,包括整個文件的摘要,還有文件中每一項的摘要;而CERT.RSA中記錄的是對CERT.SF文件的簽名,以及簽名的公鑰。
大家知道,Android平臺上所有應用程序安裝都是由PackageManangerService(代碼位于frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java)來管理的,Android的安裝流程非常復雜,與簽名驗證相關的步驟位于installPackageLI函數中:
?
[java]?view plaincopy
private?void?installPackageLI(InstallArgs?args,?PackageInstalledInfo?res)?{??????……??????PackageParser?pp?=?new?PackageParser();??????……??????try?{??????????pp.collectCertificates(pkg,?parseFlags);??????????pp.collectManifestDigest(pkg);??????}?catch?(PackageParserException?e)?{??????????res.setError("Failed?collect?during?installPackageLI",?e);??????????return;??????}??????……?? PackageParser(代碼位于frameworks\base\core\java\android\content\pm\PackageParser.java,編譯后存在于framework.jar文件中)是一個apk包的解析器,接下來我們來看其collectCertificates函數的實現:
?
?
[java]?view plaincopy
public?void?collectCertificates(Package?pkg,?int?flags)?throws?PackageParserException?{??????pkg.mCertificates?=?null;??????pkg.mSignatures?=?null;??????pkg.mSigningKeys?=?null;????????collectCertificates(pkg,?new?File(pkg.baseCodePath),?flags);??????……?? ?
接著調用了collectCertficates的一個重載版本:
[java]?view plaincopy
private?static?void?collectCertificates(Package?pkg,?File?apkFile,?int?flags)??????????????throws?PackageParserException?{??????final?String?apkPath?=?apkFile.getAbsolutePath();????????StrictJarFile?jarFile?=?null;??????try?{??????????jarFile?=?new?StrictJarFile(apkPath);??????????……?? 函數的開頭,首先創建了一個StrictJarFile(代碼位于libcore\luni\src\main\java\java\util\jar\StrictJarFile.java,編譯后存在于core.jar文件中)對象,先來看看其構造函數中的內容:
?
[java]?view plaincopy
public?StrictJarFile(String?fileName)?throws?IOException?{??????……??????try?{??????????HashMap<String,?byte[]>?metaEntries?=?getMetaEntries();??????????this.manifest?=?new?Manifest(metaEntries.get(JarFile.MANIFEST_NAME),?true);??????????this.verifier?=?new?JarVerifier(fileName,?manifest,?metaEntries);????????????isSigned?=?verifier.readCertificates()?&&?verifier.isSignedJar();??????????……?? ?
這里構造了幾個重要的對象。首先,獲得了META-INF目錄下所有文件名及其字節流。然后是構造了一個manifest對象,主要是用來處理對META-INF目錄下MANIFEST.MF文件的操作。接著,構造了一個JarVeirifer(代碼位于libcore\luni\src\main\java\java\util\jar\JarVerifier.java文件中,編譯后存在于core.jar文件中)對象,這個對象主要實現了對Jar文件的驗證工作,非常關鍵,后面的分析中會逐步提到。在構造函數的最后,調用了JarVeirifer.readCertificates函數:
[java]?view plaincopy
synchronized?boolean?readCertificates()?{??????if?(metaEntries.isEmpty())?{??????????return?false;??????}????????Iterator<String>?it?=?metaEntries.keySet().iterator();??????while?(it.hasNext())?{??????????String?key?=?it.next();??????????if?(key.endsWith(".DSA")?||?key.endsWith(".RSA")?||?key.endsWith(".EC"))?{??????????????verifyCertificate(key);??????????????it.remove();??????????}??????}??????return?true;??}?? 代碼遍歷所有META-INF目錄下的文件,找到以.DSA、.RSA或者.EC結尾的文件,以這些名字結尾的文件都是所謂的簽名證書文件。在本例中對應的是META-INF目錄下的CERT.RSA簽名文件。然后調用JarVeirifer.verifyCertificate函數:
?
[java]?view plaincopy
private?void?verifyCertificate(String?certFile)?{??????String?signatureFile?=?certFile.substring(0,?certFile.lastIndexOf('.'))?+?".SF";??????byte[]?sfBytes?=?metaEntries.get(signatureFile);??????if?(sfBytes?==?null)?{??????????return;??????}??????……??????byte[]?sBlockBytes?=?metaEntries.get(certFile);??????try?{?????????Certificate[]?signerCertChain?=?JarUtils.verifySignature(??????????????????????new?ByteArrayInputStream(sfBytes),??????????????????????new?ByteArrayInputStream(sBlockBytes));??????????if?(signerCertChain?!=?null)?{??????????????certificates.put(signatureFile,?signerCertChain);??????????}??????}?catch?(IOException?e)?{??????????return;??????}?catch?(GeneralSecurityException?e)?{??????????throw?failedVerification(jarName,?signatureFile);??????}??????……?? 函數開頭,首先找到與證書文件同名,但是以.SF結尾的簽名文件,本例中即為META-INF目錄下的CERT.SF文件。然后分別獲得簽名文件CERT.SF和證書文件CERT.RSA的字節流,調用JarUtils(代碼位于libcore\luni\src\main\java\org\apache\harmony\security\utils\JarUtils.java文件中,編譯后存在于core.jar文件中)的verifySignature函數,驗證CERT.RSA文件中包含的對CERT.SF文件的簽名是否正確。如果驗證失敗,則會拋出GeneralSecurityException異常;而如果驗證成功,則會返回簽名的證書鏈。回到JarVeirifer.verifyCertificate函數,如果JarUtils.verifySignature驗證失敗拋出異常,被捕獲后會接著向上拋出SecurityException異常;
?
?
[java]?view plaincopy
private?static?SecurityException?failedVerification(String?jarName,?String?signatureFile)?{??????throw?new?SecurityException(jarName?+?"?failed?verification?of?"?+?signatureFile);??}?? ?
而如果簽名驗證成功的話,會將證書鏈保存在certifcates屬性變量中。而JarVerifier自己的isSignedJar函數,就是判斷一下這個certificates屬性變量是否為空。
[java]?view plaincopy
boolean?isSignedJar()?{??????return?certificates.size()?>?0;??}?? 如果不為空就代表這個Jar是簽過名的,如果為空則代表其沒有簽過名。我們接著看JarVeirifer.verifyCertificate函數:
?
[java]?view plaincopy
……??Attributes?attributes?=?new?Attributes();??HashMap<String,?Attributes>?entries?=?new?HashMap<String,?Attributes>();??try?{??????ManifestReader?im?=?new?ManifestReader(sfBytes,?attributes);??????im.readEntries(entries,?null);??}?catch?(IOException?e)?{??????return;??}????if?(attributes.get(Attributes.Name.SIGNATURE_VERSION)?==?null)?{??????return;??}????boolean?createdBySigntool?=?false;??String?createdBy?=?attributes.getValue("Created-By");??if?(createdBy?!=?null)?{??????createdBySigntool?=?createdBy.indexOf("signtool")?!=?-1;??}??……?? 函數接下來讀取了所謂簽名文件,也就是META-INF目錄下CERT.SF文件中的內容。CERT.SF文件內容大致如下:
?
接著,判斷了是否有“Signature-Version”屬性,如果沒有的話,直接返回。再下來判斷apk是否是由簽名工具簽的名,判斷條件就是在“Created-By”屬性值內有沒有“signtool”字符串。本例中,簽名版本是“1.0”,并且不是用其它簽名工具簽的名。如果不是用其它工具簽名的話,接下來還會驗證主屬性中是否有“SHA1-Digest-Manifest-Main-Attributes”屬性的值,這個屬性值記錄的是對META-INF目錄下MANIFEST.MF文件內,頭屬性塊的hash值。
?
[java]?view plaincopy
……??byte[]?manifestBytes?=?metaEntries.get(JarFile.MANIFEST_NAME);??if?(manifestBytes?==?null)?{??????return;??}??……??if?(mainAttributesEnd?>?0?&&?!createdBySigntool)?{??????String?digestAttribute?=?"-Digest-Manifest-Main-Attributes";??????if?(!verify(attributes,?digestAttribute,?manifestBytes,?0,?mainAttributesEnd,?false,?true))?{??????????throw?failedVerification(jarName,?signatureFile);??????}??}??……?? 接著調用了JarVerifier.verify對該摘要值進行驗證:
[java]?view plaincopy
private?boolean?verify(Attributes?attributes,?String?entry,?byte[]?data,??????????????int?start,?int?end,?boolean?ignoreSecondEndline,?boolean?ignorable)?{??????for?(int?i?=?0;?i?<?DIGEST_ALGORITHMS.length;?i++)?{??????????String?algorithm?=?DIGEST_ALGORITHMS[i];??????????String?hash?=?attributes.getValue(algorithm?+?entry);??????????if?(hash?==?null)?{??????????????continue;??????????}????????????MessageDigest?md;??????????try?{??????????????md?=?MessageDigest.getInstance(algorithm);??????????}?catch?(NoSuchAlgorithmException?e)?{??????????????continue;??????????}??????????if?(ignoreSecondEndline?&&?data[end?-?1]?==?'\n'?&&?data[end?-?2]?==?'\n')?{??????????????md.update(data,?start,?end?-?1?-?start);??????????}?else?{??????????????md.update(data,?start,?end?-?start);??????????}??????????byte[]?b?=?md.digest();??????????byte[]?hashBytes?=?hash.getBytes(StandardCharsets.ISO_8859_1);??????????return?MessageDigest.isEqual(b,?Base64.decode(hashBytes));??????}??????return?ignorable;??}?? JarVerifier.verify函數很簡單,由于不知道到底是用什么算法算出的散列值,所以其會遍歷所有的可能算法。這些算法都預先定義在DIGEST_ALGORITHMS這個JarVerifier內的靜態字符串數組變量中:
[java]?view plaincopy
private?static?final?String[]?DIGEST_ALGORITHMS?=?new?String[]?{??????"SHA-512",??????"SHA-384",??????"SHA-256",??????"SHA1",??};?? ?
可以看出,一共支持四種算法,本例中用到的是SHA1摘要算法。變量attributes表示的是一個屬性塊,而變量entry是要在attributes屬性塊中查找的屬性名的一部分,它會與摘要算法的名稱拼接成正真的屬性名。接著會將在屬性塊中,對應屬性名的屬性值取出來,與data數據塊中start到end之間的數據,用同樣算法算出的摘要值進行比較,如果一致就返回“true”,不一致則返回“false”。
而ignorable表示這個驗證是否可忽略,也就是說如果要查找的屬性不存在的情況下,如果可忽略,則仍然返回“true”。但如果屬性值確實存在則這項對判斷結果沒有任何影響。本例中,根本沒有這個屬性,但是驗證任然是通過的,因為在調用的時候,最后一個參數ignorable被設置成了“true”。
?
[java]?view plaincopy
????……??????String?digestAttribute?=?createdBySigntool???"-Digest"?:?"-Digest-Manifest";??????if?(!verify(attributes,?digestAttribute,?manifestBytes,?0,?manifestBytes.length,?false,?false))?{??????????Iterator<Map.Entry<String,?Attributes>>?it?=?entries.entrySet().iterator();??????????while?(it.hasNext())?{??????????????Map.Entry<String,?Attributes>?entry?=?it.next();??????????????Manifest.Chunk?chunk?=?manifest.getChunk(entry.getKey());??????????????if?(chunk?==?null)?{??????????????????return;??????????????}??????????????if?(!verify(entry.getValue(),?"-Digest",?manifestBytes,??????????????????????chunk.start,?chunk.end,?createdBySigntool,?false))?{??????????????????throw?invalidDigest(signatureFile,?entry.getKey(),?jarName);??????????????}??????????}??????}??????metaEntries.put(signatureFile,?null);??????signatures.put(signatureFile,?entries);??}?? ?
JarVeirifer.verifyCertificate剩下的代碼就很簡單了,會比較MANIFEST.MF文件的整體摘要值和每一個屬性塊的摘要值,與CERT.SF文件中記錄的是否一致。如果都驗證通過的話,會將該簽名文件的信息加到metaEntries和signatures屬性變量中去。
所以,在StrictJarFile構造的過程中就已經完成了兩步驗證:一是通過在CERT.RSA文件中記錄的簽名信息,驗證了CERT.SF沒有被篡改過;二是通過CERT.SF文件中記錄的摘要值,驗證了MANIFEST.MF沒有被修改過。
所以,到目前為止,還有一步沒有被驗證,即apk內文件的摘要值要與MANIFEST.MF文件中記錄的一致。接下來,讓我們繼續回到PackageParser. collectCertificates函數中:
?
[java]?view plaincopy
……??final?ZipEntry?manifestEntry?=?jarFile.findEntry(ANDROID_MANIFEST_FILENAME);??if?(manifestEntry?==?null)?{??????throw?new?PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,??????????????????"Package?"?+?apkPath?+?"?has?no?manifest");??}????final?List<ZipEntry>?toVerify?=?new?ArrayList<>();??toVerify.add(manifestEntry);????if?((flags?&?PARSE_IS_SYSTEM)?==?0)?{??????final?Iterator<ZipEntry>?i?=?jarFile.iterator();??????while?(i.hasNext())?{??????????final?ZipEntry?entry?=?i.next();????????????if?(entry.isDirectory())?continue;??????????if?(entry.getName().startsWith("META-INF/"))?continue;??????????if?(entry.getName().equals(ANDROID_MANIFEST_FILENAME))?continue;????????????toVerify.add(entry);??????}??}??……?? 接下來的代碼主要是用來確定,到底哪些文件需要進行驗證。AndroidManifest.xml無論如何都要驗證。如果不是系統,也就是普通的應用程序安裝,必須要驗證除去位于META-INF目錄下所有文件之外的所有剩下的文件。
?
?
[java]?view plaincopy
……??for?(ZipEntry?entry?:?toVerify)?{??????final?Certificate[][]?entryCerts?=?loadCertificates(jarFile,?entry);??????if?(ArrayUtils.isEmpty(entryCerts))?{??????????throw?new?PackageParserException(??????????????????????INSTALL_PARSE_FAILED_NO_CERTIFICATES,??????????????????????"Package?"?+?apkPath?+?"?has?no?certificates?at?entry?"?+?entry.getName());??????}??????final?Signature[]?entrySignatures?=?convertToSignatures(entryCerts);??????……?? 接著是逐項驗證前面羅列出的apk中的各個文件。對每個文件,都接著調用了PackageParser.loadCertificates函數:
?
?
[java]?view plaincopy
private?static?Certificate[][]?loadCertificates(StrictJarFile?jarFile,?ZipEntry?entry)??????????????throws?PackageParserException?{??????InputStream?is?=?null;??????try?{??????????is?=?jarFile.getInputStream(entry);??????????readFullyIgnoringContents(is);??????????return?jarFile.getCertificateChains(entry);??????}?catch?(IOException?|?RuntimeException?e)?{??????????throw?new?PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,??????????????????????"Failed?reading?"?+?entry.getName()?+?"?in?"?+?jarFile,?e);??????}?finally?{??????????IoUtils.closeQuietly(is);??????}??}?? 貌似沒有什么特別的,只是對apk內的文件創建了一個輸入流,并且通過函數PackageParser.readFullyIgnoringContents全讀了一遍,而且通過函數名可以看出,具體讀出什么內容并不重要。我們先來看看StrictJarFile.getInputStream函數:
?
?
[java]?view plaincopy
public?InputStream?getInputStream(ZipEntry?ze)?{??????final?InputStream?is?=?getZipInputStream(ze);????????if?(isSigned)?{??????????JarVerifier.VerifierEntry?entry?=?verifier.initEntry(ze.getName());??????????if?(entry?==?null)?{??????????????return?is;??????????}????????????return?new?JarFile.JarFileInputStream(is,?ze.getSize(),?entry);??????}????????return?is;??}?? 重點要關注兩個函數調用,一是JarVerifier.initEntry,二是JarFile.JarFileInputStream。好,我們先來看第一個:
?
?
[java]?view plaincopy
VerifierEntry?initEntry(String?name)?{??????if?(manifest?==?null?||?signatures.isEmpty())?{??????????return?null;??????}????????Attributes?attributes?=?manifest.getAttributes(name);??????if?(attributes?==?null)?{??????????return?null;??????}????????ArrayList<Certificate[]>?certChains?=?new?ArrayList<Certificate[]>();??????Iterator<Map.Entry<String,?HashMap<String,?Attributes>>>?it?=?signatures.entrySet().iterator();??????while?(it.hasNext())?{??????????Map.Entry<String,?HashMap<String,?Attributes>>?entry?=?it.next();??????????HashMap<String,?Attributes>?hm?=?entry.getValue();??????????if?(hm.get(name)?!=?null)?{??????????????String?signatureFile?=?entry.getKey();??????????????Certificate[]?certChain?=?certificates.get(signatureFile);??????????????if?(certChain?!=?null)?{??????????????????certChains.add(certChain);??????????????}??????????}??????}????????if?(certChains.isEmpty())?{??????????return?null;??????}??????Certificate[][]?certChainsArray?=?certChains.toArray(new?Certificate[certChains.size()][]);????????for?(int?i?=?0;?i?<?DIGEST_ALGORITHMS.length;?i++)?{??????????final?String?algorithm?=?DIGEST_ALGORITHMS[i];??????????final?String?hash?=?attributes.getValue(algorithm?+?"-Digest");??????????if?(hash?==?null)?{??????????????continue;??????????}??????????byte[]?hashBytes?=?hash.getBytes(StandardCharsets.ISO_8859_1);????????????try?{??????????????return?new?VerifierEntry(name,?MessageDigest.getInstance(algorithm),?hashBytes,??????????????????????????certChainsArray,?verifiedEntries);??????????}?catch?(NoSuchAlgorithmException?ignored)?{??????????}??????}??????return?null;??}?? ?
該函數主要的用途就是構造一個JarVerifer.VerifierEntry對象:
要構造這個對象,必須事先準備好參數。第一個參數很簡單,就是要驗證的文件名,直接將name傳進來就好了。第二個參數是計算摘要的對象,可以通過MessageDigest.getInstance獲得,不過要先告知到底要用哪個摘要算法,同樣也是通過查看MANIFEST.MF文件中對應名字的屬性值來決定的。本例中的MANIFEST.MF文件格式大致如下:
所以可以知道所用的摘要算法是SHA1。第三個參數是對應文件的摘要值,這是通過讀取MANIFEST.MF文件獲得的。第四個參數是證書鏈,即對該apk文件簽名的所有證書鏈信息。為什么是二維數組呢?這是因為Android允許用多個證書對apk進行簽名,但是它們的證書文件名必須不同。最后一個參數是已經驗證過的文件列表,VerifierEntry在完成了對指定文件的摘要驗證之后會將該文件的信息加到其中。
生成好了entry之后,我們接下來看JarFile(代碼位于)中的JarFileInputStream函數的實現:
?
[java]?view plaincopy
static?final?class?JarFileInputStream?extends?FilterInputStream?{??????private?final?JarVerifier.VerifierEntry?entry;????????private?long?count;??????private?boolean?done?=?false;????????JarFileInputStream(InputStream?is,?long?size,?JarVerifier.VerifierEntry?e)?{??????????super(is);??????????entry?=?e;????????????count?=?size;??????}??????……?? 其構造函數沒有什么特別的,只是完成了賦值的操作。所以,調用StrictJarFile.getInputStream函數之后,實際返回的是一個JarFileInputStream對象。在獲得了這個輸入流對象后,緊接著,PackageParser.loadCertificates會調用PackageParser .readFullyIgnoringContents對這個輸入流進行讀取的操作:
[java]?view plaincopy
public?static?long?readFullyIgnoringContents(InputStream?in)?throws?IOException?{??????byte[]?buffer?=?sBuffer.getAndSet(null);??????if?(buffer?==?null)?{??????????buffer?=?new?byte[4096];??????}????????int?n?=?0;??????int?count?=?0;??????while?((n?=?in.read(buffer,?0,?buffer.length))?!=?-1)?{??????????count?+=?n;??????}????????sBuffer.set(buffer);??????return?count;??}?? 沒什么特別的,只是調用了InputStream的read函數,直到讀完為止,而且只是返回了讀到了多少個字節,并沒有返回讀到的內容,所以讀到什么內容它并不關心。由于實際傳進來的是InputStream的子類,這里也就是JarFileInputStream,它對read函數進行了重載,看它是如何實現的:
?
?
[java]?view plaincopy
public?int?read(byte[]?buffer,?int?byteOffset,?int?byteCount)?throws?IOException?{??????if?(done)?{??????????return?-1;??????}??????if?(count?>?0)?{??????????int?r?=?super.read(buffer,?byteOffset,?byteCount);??????????if?(r?!=?-1)?{??????????????int?size?=?r;??????????????if?(count?<?size)?{??????????????????size?=?(int)?count;??????????????}??????????????entry.write(buffer,?byteOffset,?size);??????????????count?-=?size;??????????}?else?{??????????????count?=?0;??????????}??????????if?(count?==?0)?{??????????????done?=?true;??????????????entry.verify();??????????}??????????return?r;??????}?else?{??????????done?=?true;??????????entry.verify();??????????return?-1;??????}??}?? 玄機原來在這里,這里的JarFileInputStream.read確實會調用其父類的read讀取指定的apk內文件的內容,并且將其傳給JarVerifier.VerifierEntry.write函數。當文件讀完后,會接著調用JarVerifier.VerifierEntry.verify函數對其進行驗證。JarVerifier.VerifierEntry.write函數非常簡單:
?
?
[java]?view plaincopy
public?void?write(byte[]?buf,?int?off,?int?nbytes)?{??????digest.update(buf,?off,?nbytes);??}?? ?
就是將讀到的文件的內容傳給digest,這個digest就是前面在構造JarVerifier.VerifierEntry傳進來的,對應于在MANIFEST.MF文件中指定的摘要算法。萬事具備,接下來想要驗證就很簡單了:
[java]?view plaincopy
void?verify()?{??????byte[]?d?=?digest.digest();??????if?(!MessageDigest.isEqual(d,?Base64.decode(hash)))?{??????????throw?invalidDigest(JarFile.MANIFEST_NAME,?name,?name);??????}??????verifiedEntries.put(name,?certChains);??}?? 通過digest就可以算出apk內指定文件的真實摘要值。而記錄在MANIFEST.MF文件中對應該文件的摘要值,也在構造JarVerifier.VerifierEntry時傳遞給了hash變量。不過這個hash值是經過Base64編碼的。所以在比較之前,必須通過Base64解碼。如果不一致的話,會拋出SecurityException異常:
?
[java]?view plaincopy
private?static?SecurityException?invalidDigest(String?signatureFile,?String?name,??????????String?jarName)?{??????throw?new?SecurityException(signatureFile?+?"?has?invalid?digest?for?"?+?name?+??????????????"?in?"?+?jarName);??}?? 至此,最后一步驗證,即apk內所有文件的摘要值要和在MANIFEST.MF文件中記錄的一致,也已經完成了。這還沒完,PackageParser.collectCertificates還要接著驗證apk文件中的每個文件對應的簽名要和第一個文件一致:
?
?
[java]?view plaincopy
……??if?(pkg.mCertificates?==?null)?{??????pkg.mCertificates?=?entryCerts;??????pkg.mSignatures?=?entrySignatures;??????pkg.mSigningKeys?=?new?ArraySet<PublicKey>();??????for?(int?i=0;?i?<?entryCerts.length;?i++)?{??????????pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());??????}??}?else?{??????if?(!Signature.areExactMatch(pkg.mSignatures,?entrySignatures))?{??????????throw?new?PackageParserException(??????????????INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,???????????????"Package?"?+?apkPath?+?"?has?mismatched?certificates?at?entry?"???????????????+?entry.getName());??????}??}??……?? ?
到這里,apk安裝時的簽名驗證過程都已經分析完了,來總結一下:
?
所有有關apk文件的簽名驗證工作都是在JarVerifier里面做的,一共分成三步;JarVeirifer.verifyCertificate主要做了兩步。首先,使用證書文件(在META-INF目錄下,以.DSA、.RSA或者.EC結尾的文件)檢驗簽名文件(在META-INF目錄下,和證書文件同名,但擴展名為.SF的文件)是沒有被修改過的。然后,使用簽名文件,檢驗MANIFEST.MF文件中的內容也沒有被篡改過;JarVerifier.VerifierEntry.verify做了最后一步驗證,即保證apk文件中包含的所有文件,對應的摘要值與MANIFEST.MF文件中記錄的一致。 ?
轉載于:https://www.cnblogs.com/mjblogs/p/5066880.html
總結
以上是生活随笔為你收集整理的Android应用程序签名过程和解析过程分析的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。