一、前言
Android 簽名。
在說道Android簽名之前,我們需要了解的幾個(gè)知識(shí)點(diǎn)
1、數(shù)據(jù)摘要(數(shù)據(jù)指紋)、簽名文件,證書文件
2、jarsign工具簽名和signapk工具簽名
3、keystore文件和pk8文件,x509.pem文件的關(guān)系
4、如何手動(dòng)的簽名apk
上面介紹的四個(gè)知識(shí)點(diǎn),就是今天介紹的核心,我們來一一看這些問題。
二、準(zhǔn)備知識(shí)
首先來看一下數(shù)據(jù)摘要,簽名文件,證書文件的知識(shí)點(diǎn)
1、數(shù)據(jù)摘要
這個(gè)知識(shí)點(diǎn)很好理解,百度百科即可,其實(shí)他也是一種算法,就是對(duì)一個(gè)數(shù)據(jù)源進(jìn)行一個(gè)算法之后得到一個(gè)摘要,也叫作數(shù)據(jù)指紋,不同的數(shù)據(jù)源,數(shù)據(jù)指紋肯定不一樣,就和人一樣。
消息摘要算法(Message Digest Algorithm)是一種能產(chǎn)生特殊輸出格式的算法,其原理是根據(jù)一定的運(yùn)算規(guī)則對(duì)原始數(shù)據(jù)進(jìn)行某種形式的信息提取,被提取出的信息就被稱作原始數(shù)據(jù)的消息摘要。
著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的變體。
消息摘要的主要特點(diǎn)有:
1)無論輸入的消息有多長,計(jì)算出來的消息摘要的長度總是固定的。例如應(yīng)用MD5算法摘要的消息有128個(gè)比特位,用SHA-1算法摘要的消息最終有160比特位的輸出。
2)一般來說(不考慮碰撞的情況下),只要輸入的原始數(shù)據(jù)不同,對(duì)其進(jìn)行摘要以后產(chǎn)生的消息摘要也必不相同,即使原始數(shù)據(jù)稍有改變,輸出的消息摘要便完全不同。但是,相同的輸入必會(huì)產(chǎn)生相同的輸出。
3)具有不可逆性,即只能進(jìn)行正向的信息摘要,而無法從摘要中恢復(fù)出任何的原始消息。
2、簽名文件和證書
簽名文件和證書是成對(duì)出現(xiàn)了,二者不可分離,而且我們后面通過源碼可以看到,這兩個(gè)文件的名字也是一樣的,只是后綴名不一樣。
其實(shí)數(shù)字簽名的概念很簡單。大家知道,要確??煽客ㄐ牛仨氁鉀Q兩個(gè)問題:首先,要確定消息的來源確實(shí)是其申明的那個(gè)人;其次,要保證信息在傳遞的過程中不被第三方篡改,即使被篡改了,也可以發(fā)覺出來。
所謂數(shù)字簽名,就是為了解決這兩個(gè)問題而產(chǎn)生的,它是對(duì)前面提到的非對(duì)稱加密技術(shù)與數(shù)字摘要技術(shù)的一個(gè)具體的應(yīng)用。
對(duì)于消息的發(fā)送者來說,先要生成一對(duì)公私鑰對(duì),將公鑰給消息的接收者。
如果消息的發(fā)送者有一天想給消息接收者發(fā)消息,在發(fā)送的信息中,除了要包含原始的消息外,還要加上另外一段消息。這段消息通過如下兩步生成:
1)對(duì)要發(fā)送的原始消息提取消息摘要;
2)對(duì)提取的信息摘要用自己的私鑰加密。
通過這兩步得出的消息,就是所謂的原始信息的數(shù)字簽名。
而對(duì)于信息的接收者來說,他所收到的信息,將包含兩個(gè)部分,一是原始的消息內(nèi)容,二是附加的那段數(shù)字簽名。他將通過以下三步來驗(yàn)證消息的真?zhèn)危?br>
1)對(duì)原始消息部分提取消息摘要,注意這里使用的消息摘要算法要和發(fā)送方使用的一致;
2)對(duì)附加上的那段數(shù)字簽名,使用預(yù)先得到的公鑰解密;
3)比較前兩步所得到的兩段消息是否一致。如果一致,則表明消息確實(shí)是期望的發(fā)送者發(fā)的,且內(nèi)容沒有被篡改過;相反,如果不一致,則表明傳送的過程中一定出了問題,消息不可信。
通過這種所謂的數(shù)字簽名技術(shù),確實(shí)可以有效解決可靠通信的問題。如果原始消息在傳送的過程中被篡改了,那么在消息接收者那里,對(duì)被篡改的消息提取的摘要肯定和原始的不一樣。并且,由于篡改者沒有消息發(fā)送方的私鑰,即使他可以重新算出被篡改消息的摘要,也不能偽造出數(shù)字簽名。
所以,綜上所述,數(shù)字簽名其實(shí)就是只有信息的發(fā)送者才能產(chǎn)生的別人無法偽造的一段數(shù)字串,這段數(shù)字串同時(shí)也是對(duì)信息的發(fā)送者發(fā)送信息真實(shí)性的一個(gè)有效證明。
不知道大家有沒有注意,前面講的這種數(shù)字簽名方法,有一個(gè)前提,就是消息的接收者必須要事先得到正確的公鑰。如果一開始公鑰就被別人篡改了,那壞人就會(huì)被你當(dāng)成好人,而真正的消息發(fā)送者給你發(fā)的消息會(huì)被你視作無效的。而且,很多時(shí)候根本就不具備事先溝通公鑰的信息通道。那么如何保證公鑰的安全可信呢?這就要靠數(shù)字證書來解決了。
所謂數(shù)字證書,一般包含以下一些內(nèi)容:
證書的發(fā)布機(jī)構(gòu)(Issuer)
證書的有效期(Validity)
消息發(fā)送方的公鑰
證書所有者(Subject)
數(shù)字簽名所使用的算法
數(shù)字簽名
可以看出,數(shù)字證書其實(shí)也用到了數(shù)字簽名技術(shù)。只不過要簽名的內(nèi)容是消息發(fā)送方的公鑰,以及一些其它信息。但與普通數(shù)字簽名不同的是,數(shù)字證書中簽名者不是隨隨便便一個(gè)普通的機(jī)構(gòu),而是要有一定公信力的機(jī)構(gòu)。這就好像你的大學(xué)畢業(yè)證書上簽名的一般都是德高望重的校長一樣。一般來說,這些有公信力機(jī)構(gòu)的根證書已經(jīng)在設(shè)備出廠前預(yù)先安裝到了你的設(shè)備上了。所以,數(shù)字證書可以保證數(shù)字證書里的公鑰確實(shí)是這個(gè)證書的所有者的,或者證書可以用來確認(rèn)對(duì)方的身份。數(shù)字證書主要是用來解決公鑰的安全發(fā)放問題。
綜上所述,總結(jié)一下,數(shù)字簽名和簽名驗(yàn)證的大體流程如下圖所示:

3、jarsign和signapk工具
了解到完了簽名中的三個(gè)文件的知識(shí)點(diǎn)之后,下面繼續(xù)來看看Android中簽名的兩個(gè)工具:jarsign和signapk
關(guān)于這兩個(gè)工具開始的時(shí)候很容易混淆,感覺他們兩到底有什么區(qū)別嗎?
其實(shí)這兩個(gè)工具很好理解,jarsign是Java本生自帶的一個(gè)工具,他可以對(duì)jar進(jìn)行簽名的。而signapk是后面專門為了Android應(yīng)用程序apk進(jìn)行簽名的工具,他們兩的簽名算法沒什么區(qū)別,主要是簽名時(shí)使用的文件不一樣,這個(gè)就要引出第三個(gè)問題了。
4、keystore文件和pk8,x509.pem文件的區(qū)別
我們上面了解到了jarsign和signapk兩個(gè)工具都可以進(jìn)行Android中的簽名,那么他們的區(qū)別在于簽名時(shí)使用的文件不一樣
jarsign工具簽名時(shí)使用的是keystore文件
signapk工具簽名時(shí)使用的是pk8,x509.pem文件
其中我們?cè)谑褂肊clipse工具寫程序的時(shí)候,出Debug包的時(shí)候,默認(rèn)用的是jarsign工具進(jìn)行簽名的,而且Eclipse中有一個(gè)默認(rèn)簽名文件:

我們可以看到這個(gè)默認(rèn)簽名的keystore文件,當(dāng)然我們可以選擇我們自己指定的keystore文件。
這里還有一個(gè)知識(shí)點(diǎn):
我們看到上面有MD5和SHA1的摘要,這個(gè)就是keystore文件中私鑰的數(shù)據(jù)摘要,這個(gè)信息也是我們?cè)谏暾?qǐng)很多開發(fā)平臺(tái)賬號(hào)的時(shí)候需要填入的信息,比如申請(qǐng)百度地圖,微信SDK等,會(huì)需要填寫應(yīng)用的MD5或者是SHA1信息。
5、手動(dòng)的簽名Apk包
1》使用keytool和jarsigner來進(jìn)行簽名
當(dāng)然,我們?cè)谡胶灻巖elease包的時(shí)候,我們需要?jiǎng)?chuàng)建一個(gè)自己的keystore文件:


這里我們可以對(duì)keystore文件起自己的名字,而且后綴名也是無關(guān)緊要的。創(chuàng)建完文件之后,也會(huì)生成MD5和SHA1的值,這個(gè)值可以不用記錄的,可以通過命令查看keystore文件的MD5和SHA1的值。
keytool -list -keystore debug.keystore

當(dāng)然我們都知道這個(gè)keytstore文件的重要性,說白了就相當(dāng)于你的銀行卡密碼。你懂得。
這里我們看到用Eclipse自動(dòng)簽名和生成一個(gè)keystore文件,我們也可以使用keytool工具生成一個(gè)keystore文件。這個(gè)方法網(wǎng)上有,這里就不做太多的介紹了。然后我們可以使用jarsign來對(duì)apk包進(jìn)行簽名了。
我們可以手動(dòng)的生成一個(gè)keystore文件:
keytool -genkeypair -v -keyalg DSA -keysize 1024 -sigalg SHA1withDSA -validity 20000 -keystore D:\jiangwei.keystore -alias jiangwei -keypass jiangwei -storepass jiangwei

這個(gè)命令有點(diǎn)長,有幾個(gè)重要的參數(shù)需要說明:
-alias是定義別名,這里為debug
-keyalg是規(guī)定簽名算法,這里是DSA,這里的算法直接關(guān)系到后面apk中簽名文件的后綴名,到后面會(huì)詳細(xì)說明
在用jarsigner工具進(jìn)行簽名
jarsigner -verbose -sigalg SHA1withDSA -digestalg SHA1 -keystore D:\jiangwei.keystore -storepass jiangwei D:\123.apk jiangwei

這樣我們就成功的對(duì)apk進(jìn)行簽名了。
簽名的過程中遇到的問題:
1》證書鏈找不到的問題

這個(gè)是因?yàn)樽詈笠粋€(gè)參數(shù)alias,是keystore的別名輸錯(cuò)了。
2》生成keystore文件的時(shí)候提示密碼錯(cuò)誤

這個(gè)原因是因?yàn)樵诋?dāng)前目錄已經(jīng)有debug.ketystore了,在生成一個(gè)debug.keystore的話,就會(huì)報(bào)錯(cuò)
3》找不到別名的問題

這個(gè)問題的原因是因?yàn)槲覀冊(cè)谑褂胟eytool生成keystore的時(shí)候,起了debug的別名,這個(gè)問題困擾了我很久,最后做了很多例子才發(fā)現(xiàn)的,就是只要我們的keystore文件的別名是debug的話,就會(huì)報(bào)這樣的錯(cuò)誤。這個(gè)應(yīng)該和系統(tǒng)默認(rèn)的簽名debug.keystore中的別名是debug有關(guān)系吧?沒有找到j(luò)arsigner的源碼,所以只能猜測了,但是這三個(gè)問題在這里標(biāo)注一下,以防以后在遇到。
注意:Android中是允許使用多個(gè)keystore對(duì)apk進(jìn)行簽名的,這里我就不在粘貼命令了,我又創(chuàng)建了幾個(gè)keystore對(duì)apk進(jìn)行簽名:

這里我把簽名之后的apk進(jìn)行解壓之后,發(fā)現(xiàn)有三個(gè)簽名文件和證書(.SF/.DSA)
這里我也可以注意到,我們簽名時(shí)用的是DSA算法,這里的文件后綴名就是DSA
而且文件名是keystore的別名
哎,這里算是理清楚了我們上面的如何使用keytool產(chǎn)生keystore以及,用jarsigner來進(jìn)行簽名。
2》使用signapk來進(jìn)行簽名
下面我們?cè)賮砜纯磗ignapk工具進(jìn)行簽名:
java -jar signapk.jar .testkey.x509.pem testkey.pk8 debug.apk debug.sig.apk
這里需要兩個(gè)文件:.pk8和.x509.pem這兩個(gè)文件
pk8是私鑰文件
x509.pem是含有公鑰的文件
這里簽名的話就不在演示了,這里沒什么問題的。
但是這里需要注意的是:signapk簽名之后的apk中的META-INF文件夾中的三個(gè)文件的名字是這樣的,因?yàn)閟ignapk在前面的時(shí)候不像jarsigner會(huì)自動(dòng)使用別名來命名文件,這里就是寫死了是CERT的名字,不過文件名不影響的,后面分析Android中的Apk校驗(yàn)過程中會(huì)說道,只會(huì)通過后綴名來查找文件。

3》兩種的簽名方式有什么區(qū)別
那么問題來了,jarsigner簽名時(shí)用的是keystore文件,signapk簽名時(shí)用的是pk8和x509.pem文件,而且都是給apk進(jìn)行簽名的,那么keystore文件和pk8,x509.pem他們之間是不是有什么聯(lián)系呢?答案是肯定的,網(wǎng)上搜了一下,果然他們之間是可以轉(zhuǎn)化的,這里就不在分析如何進(jìn)行轉(zhuǎn)化的,網(wǎng)上的例子貌似很多,有專門的的工具可以進(jìn)行轉(zhuǎn)化:

那么到這里我們就弄清楚了這兩個(gè)簽名工具的區(qū)別和聯(lián)系。
三、分析Android中簽名流程機(jī)制
下面我們開始從源碼的角度去看看Android中的簽名機(jī)制和原理流程
因?yàn)榫W(wǎng)上沒有找到j(luò)arsigner的源碼,但是找到了signapk的源碼,那么下面我們就來看看signapk的源碼吧:
源碼位置:com/android/signapk/sign.java
通過上面的簽名時(shí)我們可以看到,Android簽名apk之后,會(huì)有一個(gè)META-INF文件夾,這里有三個(gè)文件:
MANIFEST.MF
CERT.RSA
CERT.SF
下面來看看這三個(gè)文件到底是干啥的?
1、MANIFEST.MF

我們來看看源碼:
public static void main(String[] args) { if (args.length != 4) { System.err.println("Usage: signapk " + "publickey.x509[.pem] privatekey.pk8 " + "input.jar output.jar"); System.exit(2); } JarFile inputJar = null; JarOutputStream outputJar = null; try { X509Certificate publicKey = readPublicKey(new File(args[0])); // Assume the certificate is valid for at least an hour. long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; PrivateKey privateKey = readPrivateKey(new File(args[1])); inputJar = new JarFile(new File(args[2]), false); // Don't verify. outputJar = new JarOutputStream(new FileOutputStream(args[3])); outputJar.setLevel(9); JarEntry je; // MANIFEST.MF Manifest manifest = addDigestsToManifest(inputJar); je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); manifest.write(outputJar); // CERT.SF Signature signature = Signature.getInstance("SHA1withRSA"); signature.initSign(privateKey); je = new JarEntry(CERT_SF_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); writeSignatureFile(manifest, new SignatureOutputStream(outputJar, signature)); // CERT.RSA je = new JarEntry(CERT_RSA_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); writeSignatureBlock(signature, publicKey, outputJar); // Everything else copyFiles(manifest, inputJar, outputJar, timestamp); } catch (Exception e) { e.printStackTrace(); System.exit(1); } finally { try { if (inputJar != null) inputJar.close(); if (outputJar != null) outputJar.close(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } }}
在main函數(shù)中,我們看到需要輸入四個(gè)參數(shù),然后就做了三件事:
寫MANIFEST.MF
//MANIFEST.MFManifest manifest = addDigestsToManifest(inputJar);je = new JarEntry(JarFile.MANIFEST_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);manifest.write(outputJar);
在進(jìn)入方法看看:
/** Add the SHA1 of every file to the manifest, creating it if necessary. */private static Manifest addDigestsToManifest(JarFile jar) throws IOException, GeneralSecurityException { Manifest input = jar.getManifest(); Manifest output = new Manifest(); Attributes main = output.getMainAttributes(); if (input != null) { main.putAll(input.getMainAttributes()); } else { main.putValue("Manifest-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); } BASE64Encoder base64 = new BASE64Encoder(); MessageDigest md = MessageDigest.getInstance("SHA1"); byte[] buffer = new byte[4096]; int num; // We sort the input entries by name, and add them to the // output manifest in sorted order. We expect that the output // map will be deterministic. TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { JarEntry entry = e.nextElement(); byName.put(entry.getName(), entry); } for (JarEntry entry: byName.values()) { String name = entry.getName(); if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) { InputStream data = jar.getInputStream(entry); while ((num = data.read(buffer)) > 0) { md.update(buffer, 0, num); } Attributes attr = null; if (input != null) attr = input.getAttributes(name); attr = attr != null ? new Attributes(attr) : new Attributes(); attr.putValue("SHA1-Digest", base64.encode(md.digest())); output.getEntries().put(name, attr); } } return output;}
代碼邏輯還是很簡單的,主要看那個(gè)循環(huán)的意思:
除了三個(gè)文件(MANIFEST.MF,CERT.RSA,CERT.SF),其他的文件都會(huì)對(duì)文件內(nèi)容做一次SHA1算法,就是計(jì)算出文件的摘要信息,然后用Base64進(jìn)行編碼即可,下面我們用工具來做個(gè)案例看看是不是這樣:
首先安裝工具:HashTab
然后還有一個(gè)網(wǎng)站就是在線計(jì)算Base64:http://tomeko.net/online_tools/hex_to_base64.php?lang=en
那下面就開始我們的驗(yàn)證工作吧:
我們就來驗(yàn)證一下AndroidManifest.xml文件,首先在MANIFEST.MF文件中找到這個(gè)條目,記錄SHA1的值

然后我們安裝HashTab之后,找到AndroidManifest.xml文件,右擊,選擇Hashtab:

復(fù)制SHA-1的值:9C64812DE7373B201C294101473636A3697FD73C,到上面的那個(gè)Base64轉(zhuǎn)化網(wǎng)站,轉(zhuǎn)化一下:

nGSBLec3OyAcKUEBRzY2o2l/1zw=
和MANIFEST.MF中的條目內(nèi)容一模一樣啦啦
那么從上面的分析我們就知道了,其實(shí)MANIFEST.MF中存儲(chǔ)的是:
逐一遍歷里面的所有條目,如果是目錄就跳過,如果是一個(gè)文件,就用SHA1(或者SHA256)消息摘要算法提取出該文件的摘要然后進(jìn)行BASE64編碼后,作為“SHA1-Digest”屬性的值寫入到MANIFEST.MF文件中的一個(gè)塊中。該塊有一個(gè)“Name”屬性,其值就是該文件在apk包中的路徑。
2、下面再來看一下CERT.SF文件內(nèi)容

這里的內(nèi)容感覺和MANIFEST.MF的內(nèi)容差不多,來看看代碼吧:
//CERT.SFSignature signature = Signature.getInstance("SHA1withRSA");signature.initSign(privateKey);je = new JarEntry(CERT_SF_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);writeSignatureFile(manifest,new SignatureOutputStream(outputJar, signature));
進(jìn)入到writeSignatureFile方法中:
/** Write a .SF file with a digest the specified manifest. */private static void writeSignatureFile(Manifest manifest, OutputStream out) throws IOException, GeneralSecurityException { Manifest sf = new Manifest(); Attributes main = sf.getMainAttributes(); main.putValue("Signature-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); BASE64Encoder base64 = new BASE64Encoder(); MessageDigest md = MessageDigest.getInstance("SHA1"); PrintStream print = new PrintStream( new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8"); // Digest of the entire manifest manifest.write(print); print.flush(); main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest())); Map<String, Attributes> entries = manifest.getEntries(); for (Map.Entry<String, Attributes> entry : entries.entrySet()) { // Digest of the manifest stanza for this entry. print.print("Name: " + entry.getKey() + "\r\n"); for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { print.print(att.getKey() + ": " + att.getValue() + "\r\n"); } print.print("\r\n"); print.flush(); Attributes sfAttr = new Attributes(); sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); sf.getEntries().put(entry.getKey(), sfAttr); } sf.write(out);}
首先我們可以看到,需要對(duì)之前的MANIFEST.MF文件整個(gè)內(nèi)容做一個(gè)SHA1放到SHA1-Digest-Manifest字段中:

我們看看出入的manifest變量就是剛剛寫入了MANIFEST.MF文件的

這個(gè)我們可以驗(yàn)證一下:

然后轉(zhuǎn)化一下

看到了吧,和文件中的值是一樣的啦啦

下面我們繼續(xù)看代碼,有一個(gè)循環(huán):
Map<String, Attributes> entries = manifest.getEntries();for (Map.Entry<String, Attributes> entry : entries.entrySet()) { // Digest of the manifest stanza for this entry. print.print("Name: " + entry.getKey() + "\r\n"); for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { print.print(att.getKey() + ": " + att.getValue() + "\r\n"); } print.print("\r\n"); print.flush(); Attributes sfAttr = new Attributes(); sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); sf.getEntries().put(entry.getKey(), sfAttr);} sf.write(out);
這里還是用到了剛剛傳入的mainfest變量,遍歷他的條目內(nèi)容,然后進(jìn)行SHA算法計(jì)算在Base64一下:
其實(shí)就是對(duì)MANIFEST.MF文件中的每個(gè)條目內(nèi)容做一次SHA,在保存一下即可,做個(gè)例子驗(yàn)證一下:
用AndroidManifest.xml為例,我們把MANIFEST.MF文件中的條目拷貝保存到txt文檔中:

這里需要注意的是,我們保存之后,需要添加兩個(gè)換行,我們可以在代碼中看到邏輯:

然后我們計(jì)算txt文檔的SHA值:


看到了吧,這里計(jì)算的值是一樣的啦啦

到這里我們就知道CERT.SF文件做了什么:
1》計(jì)算這個(gè)MANIFEST.MF文件的整體SHA1值,再經(jīng)過BASE64編碼后,記錄在CERT.SF主屬性塊(在文件頭上)的“SHA1-Digest-Manifest”屬性值值下
2》逐條計(jì)算MANIFEST.MF文件中每一個(gè)塊的SHA1,并經(jīng)過BASE64編碼后,記錄在CERT.SF中的同名塊中,屬性的名字是“SHA1-Digest
3、最后我們?cè)趤砜匆幌翪ERT.RSA文件

這里我們看到的都是二進(jìn)制文件,因?yàn)镽SA文件加密了,所以我們需要用openssl命令才能查看其內(nèi)容
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text

關(guān)于這些信息,可以看下面這張圖:

我們來看一下代碼:
/** Write a .RSA file with a digital signature. */private static void writeSignatureBlock( Signature signature, X509Certificate publicKey, OutputStream out) throws IOException, GeneralSecurityException { SignerInfo signerInfo = new SignerInfo( new X500Name(publicKey.getIssuerX500Principal().getName()), publicKey.getSerialNumber(), AlgorithmId.get("SHA1"), AlgorithmId.get("RSA"), signature.sign()); PKCS7 pkcs7 = new PKCS7( new AlgorithmId[] { AlgorithmId.get("SHA1") }, new ContentInfo(ContentInfo.DATA_OID, null), new X509Certificate[] { publicKey }, new SignerInfo[] { signerInfo }); pkcs7.encodeSignedData(out);}
我們看到,這里會(huì)把之前生成的 CERT.SF文件, 用私鑰計(jì)算出簽名, 然后將簽名以及包含公鑰信息的數(shù)字證書一同寫入 CERT.RSA 中保存。CERT.RSA是一個(gè)滿足PKCS7格式的文件。
四、為何要這么來簽名
上面我們就介紹了簽名apk之后的三個(gè)文件的詳細(xì)內(nèi)容,那么下面來總結(jié)一下,Android中為何要用這種方式進(jìn)行加密簽名,這種方加密是不是最安全的呢?下面我們來分析一下,如果apk文件被篡改后會(huì)發(fā)生什么。
首先,如果你改變了apk包中的任何文件,那么在apk安裝校驗(yàn)時(shí),改變后的文件摘要信息與MANIFEST.MF的檢驗(yàn)信息不同,于是驗(yàn)證失敗,程序就不能成功安裝。
其次,如果你對(duì)更改的過的文件相應(yīng)的算出新的摘要值,然后更改MANIFEST.MF文件里面對(duì)應(yīng)的屬性值,那么必定與CERT.SF文件中算出的摘要值不一樣,照樣驗(yàn)證失敗。
最后,如果你還不死心,繼續(xù)計(jì)算MANIFEST.MF的摘要值,相應(yīng)的更改CERT.SF里面的值,那么數(shù)字簽名值必定與CERT.RSA文件中記錄的不一樣,還是失敗。
那么能不能繼續(xù)偽造數(shù)字簽名呢?不可能,因?yàn)闆]有數(shù)字證書對(duì)應(yīng)的私鑰。
所以,如果要重新打包后的應(yīng)用程序能再Android設(shè)備上安裝,必須對(duì)其進(jìn)行重簽名。
從上面的分析可以得出,只要修改了Apk中的任何內(nèi)容,就必須重新簽名,不然會(huì)提示安裝失敗,當(dāng)然這里不會(huì)分析,后面一篇文章會(huì)注重分析為何會(huì)提示安裝失敗。
五、知識(shí)點(diǎn)梳理
1、數(shù)據(jù)指紋,簽名文件,證書文件的含義
1》數(shù)據(jù)指紋就是對(duì)一個(gè)數(shù)據(jù)源做SHA/MD5算法,這個(gè)值是唯一的
2》簽名文件技術(shù)就是:數(shù)據(jù)指紋+RSA算法
3》證書文件中包含了公鑰信息和其他信息
4》在Android簽名之后,其中SF就是簽名文件,RSA就是證書文件我們可以使用openssl來查看RSA文件中的證書信息和公鑰信息
2、我們了解了Android中的簽名有兩種方式:jarsigner和signapk 這兩種方式的區(qū)別是:
1》jarsigner簽名時(shí),需要的是keystore文件,而signapk簽名的時(shí)候是pk8,x509.pem文件
2》jarsigner簽名之后的SF和RSA文件名默認(rèn)是keystore的別名,而signapk簽名之后文件名是固定的:CERT
3》Eclipse中我們?cè)谂蹹ebug程序的時(shí)候,默認(rèn)用的是jarsigner方式簽名的,用的也是系統(tǒng)默認(rèn)的debug.keystore簽名文件
4》keystore文件和pk8,x509.pem文件之間可以互相轉(zhuǎn)化
六、思考
我們?cè)诜治隽撕灻夹g(shù)之后,無意中發(fā)現(xiàn)一個(gè)問題,就是CERT.SF,MANIFEST.MF,這兩個(gè)文件中的內(nèi)容的name字段都是apk中的資源名,那么就有一個(gè)問題了,如果資源名很長,而且apk中的資源很多,那么這兩個(gè)文件就會(huì)很大,那么這里我們是不是可以優(yōu)化呢?后面在分析如何減小apk大小的文章中會(huì)繼續(xù)講解,這里先提出這個(gè)問題。
