在正式解釋Android應(yīng)用程序簽名過程之前,作為鋪墊,還得先講講最基本的一些概念。
?
非對稱加密算法
?
非對稱加密算法需要兩個密鑰:公開密鑰(簡稱公鑰)和私有密鑰(簡稱私鑰)。公鑰與私鑰是一對,如果用公鑰對數(shù)據(jù)進(jìn)行加密,只有用對應(yīng)的私鑰才能解密;如果用私鑰對數(shù)據(jù)進(jìn)行加密,那么只有用對應(yīng)的公鑰才能解密。因為加密和解密使用的是兩個不同的密鑰,所以這種算法叫作非對稱加密算法。
非對稱加密算法是數(shù)字簽名和數(shù)字證書的基礎(chǔ),大家非常熟悉的RSA就是非對稱加密算法的一種實現(xiàn)。
消息摘要算法
消息摘要算法(Message Digest Algorithm)是一種能產(chǎn)生特殊輸出格式的算法,其原理是根據(jù)一定的運算規(guī)則對原始數(shù)據(jù)進(jìn)行某種形式的信息提取,被提取出的信息就被稱作原始數(shù)據(jù)的消息摘要。著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的變體。
消息摘要的主要特點有:
?
1)無論輸入的消息有多長,計算出來的消息摘要的長度總是固定的。例如應(yīng)用MD5算法摘要的消息有128個比特位,用SHA-1算法摘要的消息最終有160比特位的輸出。
2)一般來說(不考慮碰撞的情況下),只要輸入的原始數(shù)據(jù)不同,對其進(jìn)行摘要以后產(chǎn)生的消息摘要也必不相同,即使原始數(shù)據(jù)稍有改變,輸出的消息摘要便完全不同。但是,相同的輸入必會產(chǎn)生相同的輸出。
3)具有不可逆性,即只能進(jìn)行正向的信息摘要,而無法從摘要中恢復(fù)出任何的原始消息。
數(shù)字簽名和數(shù)字證書
其實數(shù)字簽名的概念很簡單。大家知道,要確保可靠通信,必須要解決兩個問題:首先,要確定消息的來源確實是其申明的那個人;其次,要保證信息在傳遞的過程中不被第三方篡改,即使被篡改了,也可以發(fā)覺出來。
所謂數(shù)字簽名,就是為了解決這兩個問題而產(chǎn)生的,它是對前面提到的非對稱加密技術(shù)與數(shù)字摘要技術(shù)的一個具體的應(yīng)用。
對于消息的發(fā)送者來說,先要生成一對公私鑰對,將公鑰給消息的接收者。
如果消息的發(fā)送者有一天想給消息接收者發(fā)消息,在發(fā)送的信息中,除了要包含原始的消息外,還要加上另外一段消息。這段消息通過如下兩步生成:
1)對要發(fā)送的原始消息提取消息摘要;
2)對提取的信息摘要用自己的私鑰加密。
通過這兩步得出的消息,就是所謂的原始信息的數(shù)字簽名。
而對于信息的接收者來說,他所收到的信息,將包含兩個部分,一是原始的消息內(nèi)容,二是附加的那段數(shù)字簽名。他將通過以下三步來驗證消息的真?zhèn)?#xff1a;
1)對原始消息部分提取消息摘要,注意這里使用的消息摘要算法要和發(fā)送方使用的一致;
2)對附加上的那段數(shù)字簽名,使用預(yù)先得到的公鑰解密;
3)比較前兩步所得到的兩段消息是否一致。如果一致,則表明消息確實是期望的發(fā)送者發(fā)的,且內(nèi)容沒有被篡改過;相反,如果不一致,則表明傳送的過程中一定出了問題,消息不可信。
通過這種所謂的數(shù)字簽名技術(shù),確實可以有效解決可靠通信的問題。如果原始消息在傳送的過程中被篡改了,那么在消息接收者那里,對被篡改的消息提取的摘要肯定和原始的不一樣。并且,由于篡改者沒有消息發(fā)送方的私鑰,即使他可以重新算出被篡改消息的摘要,也不能偽造出數(shù)字簽名。
所以,綜上所述,數(shù)字簽名其實就是只有信息的發(fā)送者才能產(chǎn)生的別人無法偽造的一段數(shù)字串,這段數(shù)字串同時也是對信息的發(fā)送者發(fā)送信息真實性的一個有效證明。
不知道大家有沒有注意,前面講的這種數(shù)字簽名方法,有一個前提,就是消息的接收者必須要事先得到正確的公鑰。如果一開始公鑰就被別人篡改了,那壞人就會被你當(dāng)成好人,而真正的消息發(fā)送者給你發(fā)的消息會被你視作無效的。而且,很多時候根本就不具備事先溝通公鑰的信息通道。那么如何保證公鑰的安全可信呢?這就要靠數(shù)字證書來解決了。
所謂數(shù)字證書,一般包含以下一些內(nèi)容:
?
- 證書的發(fā)布機構(gòu)(Issuer)
- 證書的有效期(Validity)
- 消息發(fā)送方的公鑰
- 證書所有者(Subject)
- 數(shù)字簽名所使用的算法
- 數(shù)字簽名
?
可以看出,數(shù)字證書其實也用到了數(shù)字簽名技術(shù)。只不過要簽名的內(nèi)容是消息發(fā)送方的公鑰,以及一些其它信息。但與普通數(shù)字簽名不同的是,數(shù)字證書中簽名者不是隨隨便便一個普通的機構(gòu),而是要有一定公信力的機構(gòu)。這就好像你的大學(xué)畢業(yè)證書上簽名的一般都是德高望重的校長一樣。一般來說,這些有公信力機構(gòu)的根證書已經(jīng)在設(shè)備出廠前預(yù)先安裝到了你的設(shè)備上了。所以,數(shù)字證書可以保證數(shù)字證書里的公鑰確實是這個證書的所有者的,或者證書可以用來確認(rèn)對方的身份。數(shù)字證書主要是用來解決公鑰的安全發(fā)放問題。
綜上所述,總結(jié)一下,數(shù)字簽名和簽名驗證的大體流程如下圖所示:
?
Android應(yīng)用程序簽名流程
?
大家知道,Android采用的是開放的生態(tài)系統(tǒng),任何人都可以開發(fā)和發(fā)布應(yīng)用程序給別人使用。不像iOS,在沒有被破解的情況下只能通過App Store安裝應(yīng)用,Android在打開了“Unknow Sources”選項后,可以安裝任何來源的應(yīng)用程序,可以是第三方市場,可以是自己開發(fā)的應(yīng)用,也可以從論壇下載。
那么問題來了,對于有一些不懷好意的人,完全可以拿到一個原生的應(yīng)用,然后加入一些惡意的代碼,再發(fā)布出去,誘使別人去安裝,達(dá)到不可告人的目的。
有沒有什么辦法可以防止應(yīng)用程序在傳送的過程中被第三方惡意篡改呢?Google因此引入了應(yīng)用程序簽名機制。
它是如何工作的呢?我們先來看看簽名前后,一個apk文件到底發(fā)生了哪些變化。
首先,在沒簽名之前,apk文件內(nèi)的目錄結(jié)構(gòu)是這樣的:
?
而簽名之后,會變成這樣:
?
可以看到,多出來了一個META-INF目錄。可以肯定的是,簽名的機關(guān)就在這個目錄中,里面有三個文件:
?
其實,在Android的源代碼里包含了一個工具,可以對apk文件進(jìn)行簽名,具體的代碼位置在build\tools\signapk目錄下,通過分析其中的SignApk.java文件,可以大致了解簽名的過程。其流程大致有如下幾步:
1)打開待簽名的apk文件(由于apk其實是一個用zip壓縮的文件,其實就是用zip解壓整個apk文件),逐一遍歷里面的所有條目,如果是目錄就跳過,如果是一個文件,就用SHA1(或者SHA256)消息摘要算法提取出該文件的摘要然后進(jìn)行BASE64編碼后,作為“SHA1-Digest”屬性的值寫入到MANIFEST.MF文件中的一個塊中。該塊有一個“Name”屬性,其值就是該文件在apk包中的路徑。
2)計算這個MANIFEST.MF文件的整體SHA1值,再經(jīng)過BASE64編碼后,記錄在CERT.SF主屬性塊(在文件頭上)的“SHA1-Digest-Manifest”屬性值值下。
然后,再逐條計算MANIFEST.MF文件中每一個塊的SHA1,并經(jīng)過BASE64編碼后,記錄在CERT.SF中的同名塊中,屬性的名字是“SHA1-Digest”。
3)把之前生成的?CERT.SF文件,?用私鑰計算出簽名, 然后將簽名以及包含公鑰信息的數(shù)字證書一同寫入??CERT.RSA??中保存。CERT.RSA是一個滿足PKCS7格式的文件,可以通過openssl工具來查看簽名證書的信息。在Ubuntu或者在Windows上使用Cygwin,敲入以下命令:
?
[plain]?view plaincopy
openssl?pkcs7?-inform?DER?-in?CERT.RSA?-noout?-print_certs?–text?? 可以得到如下輸出:
?
下面我們來看看,如果apk文件被篡改后會發(fā)生什么。
?
首先,如果你改變了apk包中的任何文件,那么在apk安裝校驗時,改變后的文件摘要信息與MANIFEST.MF的檢驗信息不同,于是驗證失敗,程序就不能成功安裝。
其次,如果你對更改的過的文件相應(yīng)的算出新的摘要值,然后更改MANIFEST.MF文件里面對應(yīng)的屬性值,那么必定與CERT.SF文件中算出的摘要值不一樣,照樣驗證失敗。
最后,如果你還不死心,繼續(xù)計算MANIFEST.MF的摘要值,相應(yīng)的更改CERT.SF里面的值,那么數(shù)字簽名值必定與CERT.RSA文件中記錄的不一樣,還是失敗。
那么能不能繼續(xù)偽造數(shù)字簽名呢?不可能,因為沒有數(shù)字證書對應(yīng)的私鑰。
所以,如果要重新打包后的應(yīng)用程序能再Android設(shè)備上安裝,必須對其進(jìn)行重簽名。
總結(jié)
1)Android應(yīng)用程序簽名只是用來解決發(fā)布的應(yīng)用不被別人篡改的,其并不會對應(yīng)用程序本身進(jìn)行加密,這點不同于Windows Phone和iOS。
2)Android并不要求所有應(yīng)用程序的簽名證書都由可信任CA的根證書簽名,通過這點保證了其生態(tài)系統(tǒng)的開放性,所有人都可以用自己生成的證書對應(yīng)用程序簽名。
3)如果想修改一個已經(jīng)發(fā)布的應(yīng)用程序,哪怕是修改一張圖片,都必須對其進(jìn)行重新簽名。但是,簽原始應(yīng)用的私鑰一般是拿不到的(肯定在原始應(yīng)用程序開發(fā)者的手上,且不可能公布出去),所以只能用另外一組公私鑰對,生成一個新的證書,對重打包的應(yīng)用進(jìn)行簽名。所以重打包的apk中所帶證書的公鑰肯定和原始應(yīng)用不一樣。同時,在手機上如果想安裝一個應(yīng)用程序,應(yīng)用程序安裝器會先檢查相同包名的應(yīng)用是否已經(jīng)被安裝過,如果已經(jīng)安裝過,會繼續(xù)判斷已經(jīng)安裝的應(yīng)用和將要安裝的應(yīng)用,其所攜帶的數(shù)字證書中的公鑰是否一致。如果相同,則繼續(xù)安裝;而如果不同,則會提示用戶先卸載前面已安裝的應(yīng)用。通過這種方式來提示用戶,前后兩個應(yīng)用是不同開發(fā)者簽名的,可能有一個是李鬼。
?
Android應(yīng)用程序簽名驗證過程分析
? 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
在前面的《Android應(yīng)用程序簽名過程分析》中,我大致分析了Android應(yīng)用程序簽名的過程,接下來我將結(jié)合源代碼,分析一下Android應(yīng)用程序在安裝過程中對簽名進(jìn)行驗證的過程。
我們還是用前面的例子分析,假設(shè)簽名后,apk文件中多了一個META-INF目錄,里面有三個文件,分別是MANIFEST.MF、CERT.SF和CERT.RSA:
?
通過前面的分析,我們可以知道,MANIFEST.MF中記錄的是apk中所有文件的摘要值;CERT.SF中記錄的是對MANIFEST.MF的摘要值,包括整個文件的摘要,還有文件中每一項的摘要;而CERT.RSA中記錄的是對CERT.SF文件的簽名,以及簽名的公鑰。
大家知道,Android平臺上所有應(yīng)用程序安裝都是由PackageManangerService(代碼位于frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java)來管理的,Android的安裝流程非常復(fù)雜,與簽名驗證相關(guān)的步驟位于installPackageLI函數(shù)中:
?
[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函數(shù)的實現(xiàn):
?
?
[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);??????……?? ?
接著調(diào)用了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);??????????……?? 函數(shù)的開頭,首先創(chuàng)建了一個StrictJarFile(代碼位于libcore\luni\src\main\java\java\util\jar\StrictJarFile.java,編譯后存在于core.jar文件中)對象,先來看看其構(gòu)造函數(shù)中的內(nèi)容:
?
[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();??????????……?? ?
這里構(gòu)造了幾個重要的對象。首先,獲得了META-INF目錄下所有文件名及其字節(jié)流。然后是構(gòu)造了一個manifest對象,主要是用來處理對META-INF目錄下MANIFEST.MF文件的操作。接著,構(gòu)造了一個JarVeirifer(代碼位于libcore\luni\src\main\java\java\util\jar\JarVerifier.java文件中,編譯后存在于core.jar文件中)對象,這個對象主要實現(xiàn)了對Jar文件的驗證工作,非常關(guān)鍵,后面的分析中會逐步提到。在構(gòu)造函數(shù)的最后,調(diào)用了JarVeirifer.readCertificates函數(shù):
[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結(jié)尾的文件,以這些名字結(jié)尾的文件都是所謂的簽名證書文件。在本例中對應(yīng)的是META-INF目錄下的CERT.RSA簽名文件。然后調(diào)用JarVeirifer.verifyCertificate函數(shù):
?
[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);??????}??????……?? 函數(shù)開頭,首先找到與證書文件同名,但是以.SF結(jié)尾的簽名文件,本例中即為META-INF目錄下的CERT.SF文件。然后分別獲得簽名文件CERT.SF和證書文件CERT.RSA的字節(jié)流,調(diào)用JarUtils(代碼位于libcore\luni\src\main\java\org\apache\harmony\security\utils\JarUtils.java文件中,編譯后存在于core.jar文件中)的verifySignature函數(shù),驗證CERT.RSA文件中包含的對CERT.SF文件的簽名是否正確。如果驗證失敗,則會拋出GeneralSecurityException異常;而如果驗證成功,則會返回簽名的證書鏈。回到JarVeirifer.verifyCertificate函數(shù),如果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函數(shù),就是判斷一下這個certificates屬性變量是否為空。
[java]?view plaincopy
boolean?isSignedJar()?{??????return?certificates.size()?>?0;??}?? 如果不為空就代表這個Jar是簽過名的,如果為空則代表其沒有簽過名。我們接著看JarVeirifer.verifyCertificate函數(shù):
?
[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;??}??……?? 函數(shù)接下來讀取了所謂簽名文件,也就是META-INF目錄下CERT.SF文件中的內(nèi)容。CERT.SF文件內(nèi)容大致如下:
?
接著,判斷了是否有“Signature-Version”屬性,如果沒有的話,直接返回。再下來判斷apk是否是由簽名工具簽的名,判斷條件就是在“Created-By”屬性值內(nèi)有沒有“signtool”字符串。本例中,簽名版本是“1.0”,并且不是用其它簽名工具簽的名。如果不是用其它工具簽名的話,接下來還會驗證主屬性中是否有“SHA1-Digest-Manifest-Main-Attributes”屬性的值,這個屬性值記錄的是對META-INF目錄下MANIFEST.MF文件內(nèi),頭屬性塊的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);??????}??}??……?? 接著調(diào)用了JarVerifier.verify對該摘要值進(jìn)行驗證:
[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函數(shù)很簡單,由于不知道到底是用什么算法算出的散列值,所以其會遍歷所有的可能算法。這些算法都預(yù)先定義在DIGEST_ALGORITHMS這個JarVerifier內(nèi)的靜態(tài)字符串?dāng)?shù)組變量中:
[java]?view plaincopy
private?static?final?String[]?DIGEST_ALGORITHMS?=?new?String[]?{??????"SHA-512",??????"SHA-384",??????"SHA-256",??????"SHA1",??};?? ?
可以看出,一共支持四種算法,本例中用到的是SHA1摘要算法。變量attributes表示的是一個屬性塊,而變量entry是要在attributes屬性塊中查找的屬性名的一部分,它會與摘要算法的名稱拼接成正真的屬性名。接著會將在屬性塊中,對應(yīng)屬性名的屬性值取出來,與data數(shù)據(jù)塊中start到end之間的數(shù)據(jù),用同樣算法算出的摘要值進(jìn)行比較,如果一致就返回“true”,不一致則返回“false”。
而ignorable表示這個驗證是否可忽略,也就是說如果要查找的屬性不存在的情況下,如果可忽略,則仍然返回“true”。但如果屬性值確實存在則這項對判斷結(jié)果沒有任何影響。本例中,根本沒有這個屬性,但是驗證任然是通過的,因為在調(diào)用的時候,最后一個參數(shù)ignorable被設(shè)置成了“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構(gòu)造的過程中就已經(jīng)完成了兩步驗證:一是通過在CERT.RSA文件中記錄的簽名信息,驗證了CERT.SF沒有被篡改過;二是通過CERT.SF文件中記錄的摘要值,驗證了MANIFEST.MF沒有被修改過。
所以,到目前為止,還有一步?jīng)]有被驗證,即apk內(nèi)文件的摘要值要與MANIFEST.MF文件中記錄的一致。接下來,讓我們繼續(xù)回到PackageParser. collectCertificates函數(shù)中:
?
[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);??????}??}??……?? 接下來的代碼主要是用來確定,到底哪些文件需要進(jìn)行驗證。AndroidManifest.xml無論如何都要驗證。如果不是系統(tǒng),也就是普通的應(yīng)用程序安裝,必須要驗證除去位于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中的各個文件。對每個文件,都接著調(diào)用了PackageParser.loadCertificates函數(shù):
?
?
[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內(nèi)的文件創(chuàng)建了一個輸入流,并且通過函數(shù)PackageParser.readFullyIgnoringContents全讀了一遍,而且通過函數(shù)名可以看出,具體讀出什么內(nèi)容并不重要。我們先來看看StrictJarFile.getInputStream函數(shù):
?
?
[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;??}?? 重點要關(guān)注兩個函數(shù)調(diào)用,一是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;??}?? ?
該函數(shù)主要的用途就是構(gòu)造一個JarVerifer.VerifierEntry對象:
要構(gòu)造這個對象,必須事先準(zhǔn)備好參數(shù)。第一個參數(shù)很簡單,就是要驗證的文件名,直接將name傳進(jìn)來就好了。第二個參數(shù)是計算摘要的對象,可以通過MessageDigest.getInstance獲得,不過要先告知到底要用哪個摘要算法,同樣也是通過查看MANIFEST.MF文件中對應(yīng)名字的屬性值來決定的。本例中的MANIFEST.MF文件格式大致如下:
所以可以知道所用的摘要算法是SHA1。第三個參數(shù)是對應(yīng)文件的摘要值,這是通過讀取MANIFEST.MF文件獲得的。第四個參數(shù)是證書鏈,即對該apk文件簽名的所有證書鏈信息。為什么是二維數(shù)組呢?這是因為Android允許用多個證書對apk進(jìn)行簽名,但是它們的證書文件名必須不同。最后一個參數(shù)是已經(jīng)驗證過的文件列表,VerifierEntry在完成了對指定文件的摘要驗證之后會將該文件的信息加到其中。
生成好了entry之后,我們接下來看JarFile(代碼位于)中的JarFileInputStream函數(shù)的實現(xiàn):
?
[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;??????}??????……?? 其構(gòu)造函數(shù)沒有什么特別的,只是完成了賦值的操作。所以,調(diào)用StrictJarFile.getInputStream函數(shù)之后,實際返回的是一個JarFileInputStream對象。在獲得了這個輸入流對象后,緊接著,PackageParser.loadCertificates會調(diào)用PackageParser .readFullyIgnoringContents對這個輸入流進(jìn)行讀取的操作:
[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;??}?? 沒什么特別的,只是調(diào)用了InputStream的read函數(shù),直到讀完為止,而且只是返回了讀到了多少個字節(jié),并沒有返回讀到的內(nèi)容,所以讀到什么內(nèi)容它并不關(guān)心。由于實際傳進(jìn)來的是InputStream的子類,這里也就是JarFileInputStream,它對read函數(shù)進(jìn)行了重載,看它是如何實現(xiàn)的:
?
?
[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確實會調(diào)用其父類的read讀取指定的apk內(nèi)文件的內(nèi)容,并且將其傳給JarVerifier.VerifierEntry.write函數(shù)。當(dāng)文件讀完后,會接著調(diào)用JarVerifier.VerifierEntry.verify函數(shù)對其進(jìn)行驗證。JarVerifier.VerifierEntry.write函數(shù)非常簡單:
?
?
[java]?view plaincopy
public?void?write(byte[]?buf,?int?off,?int?nbytes)?{??????digest.update(buf,?off,?nbytes);??}?? ?
就是將讀到的文件的內(nèi)容傳給digest,這個digest就是前面在構(gòu)造JarVerifier.VerifierEntry傳進(jìn)來的,對應(yīng)于在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內(nèi)指定文件的真實摘要值。而記錄在MANIFEST.MF文件中對應(yīng)該文件的摘要值,也在構(gòu)造JarVerifier.VerifierEntry時傳遞給了hash變量。不過這個hash值是經(jīng)過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內(nèi)所有文件的摘要值要和在MANIFEST.MF文件中記錄的一致,也已經(jīng)完成了。這還沒完,PackageParser.collectCertificates還要接著驗證apk文件中的每個文件對應(yīng)的簽名要和第一個文件一致:
?
?
[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安裝時的簽名驗證過程都已經(jīng)分析完了,來總結(jié)一下:
?
所有有關(guān)apk文件的簽名驗證工作都是在JarVerifier里面做的,一共分成三步;JarVeirifer.verifyCertificate主要做了兩步。首先,使用證書文件(在META-INF目錄下,以.DSA、.RSA或者.EC結(jié)尾的文件)檢驗簽名文件(在META-INF目錄下,和證書文件同名,但擴展名為.SF的文件)是沒有被修改過的。然后,使用簽名文件,檢驗MANIFEST.MF文件中的內(nèi)容也沒有被篡改過;JarVerifier.VerifierEntry.verify做了最后一步驗證,即保證apk文件中包含的所有文件,對應(yīng)的摘要值與MANIFEST.MF文件中記錄的一致。 ?
轉(zhuǎn)載于:https://www.cnblogs.com/mjblogs/p/5066880.html
總結(jié)
以上是生活随笔為你收集整理的Android应用程序签名过程和解析过程分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。