Android簽名機制之—簽名過程詳解
一、前言
又是過了好長時間,沒寫文章的雙手都有點難受了。今天是聖誕節,還是得上班。因為前幾天有一個之前的同事,在申請微信SDK的時候,遇到簽名的問題,問了我一下,結果把我難倒了。。我說Android中的簽名大家都會熟悉的,就是為了安全,不讓別人修改你的apk,但是我們真正的有了解多少呢?所以準備兩篇文章好好介紹一下Android中籤名機制。
在說道Android簽名之前,我們需要了解的幾個知識點
1、資料摘要(資料指紋)、簽名檔案,證書檔案
2、jarsign工具簽名和signapk工具簽名
3、keystore檔案和pk8檔案,x509.pem檔案的關係
4、如何手動的簽名apk
上面介紹的四個知識點,就是今天介紹的核心,我們來一一看這些問題。
二、準備知識
首先來看一下資料摘要,簽名檔案,證書檔案的知識點
1、資料摘要
這個知識點很好理解,百度百科即可,其實他也是一種演算法,就是對一個數據源進行一個演算法之後得到一個摘要,也叫作資料指紋,不同的資料來源,資料指紋肯定不一樣,就和人一樣。
訊息摘要演算法(Message Digest Algorithm)是一種能產生特殊輸出格式的演算法,其原理是根據一定的運算規則對原始資料進行某種形式的資訊提取,被提取出的資訊就被稱作原始資料的訊息摘要。
著名的摘要演算法有RSA公司的MD5演算法和SHA-1演算法及其大量的變體。
訊息摘要的主要特點有:
1)無論輸入的訊息有多長,計算出來的訊息摘要的長度總是固定的。例如應用MD5演算法摘要的訊息有128個位元位,用SHA-1演算法摘要的訊息最終有160位元位的輸出。
2)一般來說(不考慮碰撞的情況下),只要輸入的原始資料不同,對其進行摘要以後產生的訊息摘要也必不相同,即使原始資料稍有改變,輸出的訊息摘要便完全不同。但是,相同的輸入必會產生相同的輸出。
3)具有不可逆性,即只能進行正向的資訊摘要,而無法從摘要中恢復出任何的原始訊息。
2、簽名檔案和證書
簽名檔案和證書是成對出現了,二者不可分離,而且我們後面通過原始碼可以看到,這兩個檔案的名字也是一樣的,只是字尾名不一樣。
其實數字簽名的概念很簡單。大家知道,要確保可靠通訊,必須要解決兩個問題:首先,要確定訊息的來源確實是其申明的那個人;其次,要保證資訊在傳遞的過程中不被第三方篡改,即使被篡改了,也可以發覺出來。
所謂數字簽名,就是為了解決這兩個問題而產生的,它是對前面提到的非對稱加密技術與數字摘要技術的一個具體的應用。
對於訊息的傳送者來說,先要生成一對公私鑰對,將公鑰給訊息的接收者。
如果訊息的傳送者有一天想給訊息接收者發訊息,在傳送的資訊中,除了要包含原始的訊息外,還要加上另外一段訊息。這段訊息通過如下兩步生成:
1)對要傳送的原始訊息提取訊息摘要;
2)對提取的資訊摘要用自己的私鑰加密。
通過這兩步得出的訊息,就是所謂的原始資訊的數字簽名。
而對於資訊的接收者來說,他所收到的資訊,將包含兩個部分,一是原始的訊息內容,二是附加的那段數字簽名。他將通過以下三步來驗證訊息的真偽:
1)對原始訊息部分提取訊息摘要,注意這裡使用的訊息摘要演算法要和傳送方使用的一致;
2)對附加上的那段數字簽名,使用預先得到的公鑰解密;
3)比較前兩步所得到的兩段訊息是否一致。如果一致,則表明訊息確實是期望的傳送者發的,且內容沒有被篡改過;相反,如果不一致,則表明傳送的過程中一定出了問題,訊息不可信。
通過這種所謂的數字簽名技術,確實可以有效解決可靠通訊的問題。如果原始訊息在傳送的過程中被篡改了,那麼在訊息接收者那裡,對被篡改的訊息提取的摘要肯定和原始的不一樣。並且,由於篡改者沒有訊息傳送方的私鑰,即使他可以重新算出被篡改訊息的摘要,也不能偽造出數字簽名。
所以,綜上所述,數字簽名其實就是隻有資訊的傳送者才能產生的別人無法偽造的一段數字串,這段數字串同時也是對資訊的傳送者傳送資訊真實性的一個有效證明。
不知道大家有沒有注意,前面講的這種數字簽名方法,有一個前提,就是訊息的接收者必須要事先得到正確的公鑰。如果一開始公鑰就被別人篡改了,那壞人就會被你當成好人,而真正的訊息傳送者給你發的訊息會被你視作無效的。而且,很多時候根本就不具備事先溝通公鑰的資訊通道。那麼如何保證公鑰的安全可信呢?這就要靠數字證書來解決了。
所謂數字證書,一般包含以下一些內容:
證書的釋出機構(Issuer)
證書的有效期(Validity)
訊息傳送方的公鑰
證書所有者(Subject)
數字簽名所使用的演算法
數字簽名
可以看出,數字證書其實也用到了數字簽名技術。只不過要簽名的內容是訊息傳送方的公鑰,以及一些其它資訊。但與普通數字簽名不同的是,數字證書中籤名者不是隨隨便便一個普通的機構,而是要有一定公信力的機構。這就好像你的大學畢業證書上簽名的一般都是德高望重的校長一樣。一般來說,這些有公信力機構的根證書已經在裝置出廠前預先安裝到了你的裝置上了。所以,數字證書可以保證數字證書裡的公鑰確實是這個證書的所有者的,或者證書可以用來確認對方的身份。數字證書主要是用來解決公鑰的安全發放問題。
綜上所述,總結一下,數字簽名和簽名驗證的大體流程如下圖所示:
3、jarsign和signapk工具
瞭解到完了簽名中的三個檔案的知識點之後,下面繼續來看看Android中籤名的兩個工具:jarsign和signapk
關於這兩個工具開始的時候很容易混淆,感覺他們兩到底有什麼區別嗎?
其實這兩個工具很好理解,jarsign是Java本生自帶的一個工具,他可以對jar進行簽名的。而signapk是後面專門為了Android應用程式apk進行簽名的工具,他們兩的簽名演算法沒什麼區別,主要是簽名時使用的檔案不一樣,這個就要引出第三個問題了。
4、keystore檔案和pk8,x509.pem檔案的區別
我們上面瞭解到了jarsign和signapk兩個工具都可以進行Android中的簽名,那麼他們的區別在於簽名時使用的檔案不一樣
jarsign工具簽名時使用的是keystore檔案
signapk工具簽名時使用的是pk8,x509.pem檔案
其中我們在使用Eclipse工具寫程式的時候,出Debug包的時候,預設用的是jarsign工具進行簽名的,而且Eclipse中有一個預設簽名檔案:
我們可以看到這個預設簽名的keystore檔案,當然我們可以選擇我們自己指定的keystore檔案。
這裡還有一個知識點:
我們看到上面有MD5和SHA1的摘要,這個就是keystore檔案中私鑰的資料摘要,這個資訊也是我們在申請很多開發平臺賬號的時候需要填入的資訊,比如申請百度地圖,微信SDK等,會需要填寫應用的MD5或者是SHA1資訊。
5、手動的簽名Apk包
1》使用keytool和jarsigner來進行簽名
當然,我們在正式簽名處release包的時候,我們需要建立一個自己的keystore檔案:
這裡我們可以對keystore檔案起自己的名字,而且字尾名也是無關緊要的。建立完檔案之後,也會生成MD5和SHA1的值,這個值可以不用記錄的,可以通過命令檢視keystore檔案的MD5和SHA1的值。
keytool -list -keystore debug.keystore
當然我們都知道這個keytstore檔案的重要性,說白了就相當於你的銀行卡密碼。你懂得。
這裡我們看到用Eclipse自動簽名和生成一個keystore檔案,我們也可以使用keytool工具生成一個keystore檔案。這個方法網上有,這裡就不做太多的介紹了。然後我們可以使用jarsign來對apk包進行簽名了。
我們可以手動的生成一個keystore檔案:
keytool -genkeypair -v -keyalg DSA -keysize 1024 -sigalg SHA1withDSA -validity 20000 -keystore D:\jiangwei.keystore -alias jiangwei -keypass jiangwei -storepass jiangwei
這個命令有點長,有幾個重要的引數需要說明:
-alias是定義別名,這裡為debug
-keyalg是規定簽名演算法,這裡是DSA,這裡的演算法直接關係到後面apk中籤名檔案的字尾名,到後面會詳細說明
在用jarsigner工具進行簽名
jarsigner -verbose -sigalg SHA1withDSA -digestalg SHA1 -keystore D:\jiangwei.keystore -storepass jiangwei D:\123.apk jiangwei
這樣我們就成功的對apk進行簽名了。
簽名的過程中遇到的問題:
1》證書鏈找不到的問題
這個是因為最後一個引數alias,是keystore的別名輸錯了。
2》生成keystore檔案的時候提示密碼錯誤 這個原因是因為在當前目錄已經有debug.ketystore了,在生成一個debug.keystore的話,就會報錯 3》找不到別名的問題 這個問題的原因是因為我們在使用keytool生成keystore的時候,起了debug的別名,這個問題困擾了我很久,最後做了很多例子才發現的,就是隻要我們的keystore檔案的別名是debug的話,就會報這樣的錯誤。這個應該和系統預設的簽名debug.keystore中的別名是debug有關係吧?沒有找到jarsigner的原始碼,所以只能猜測了,但是這三個問題在這裡標註一下,以防以後在遇到。注意:Android中是允許使用多個keystore對apk進行簽名的,這裡我就不在貼上命令了,我又建立了幾個keystore對apk進行簽名:
這裡我把簽名之後的apk進行解壓之後,發現有三個簽名檔案和證書(.SF/.DSA)
這裡我也可以注意到,我們簽名時用的是DSA演算法,這裡的檔案字尾名就是DSA
而且檔名是keystore的別名
哎,這裡算是理清楚了我們上面的如何使用keytool產生keystore以及,用jarsigner來進行簽名。
2》使用signapk來進行簽名
下面我們再來看看signapk工具進行簽名:
java -jar signapk.jar .testkey.x509.pem testkey.pk8 debug.apk debug.sig.apk
這裡需要兩個檔案:.pk8和.x509.pem這兩個檔案
pk8是私鑰檔案
x509.pem是含有公鑰的檔案
這裡簽名的話就不在演示了,這裡沒什麼問題的。
但是這裡需要注意的是:signapk簽名之後的apk中的META-INF資料夾中的三個檔案的名字是這樣的,因為signapk在前面的時候不像jarsigner會自動使用別名來命名檔案,這裡就是寫死了是CERT的名字,不過檔名不影響的,後面分析Android中的Apk校驗過程中會說道,只會通過後綴名來查詢檔案。
3》兩種的簽名方式有什麼區別
那麼問題來了,jarsigner簽名時用的是keystore檔案,signapk簽名時用的是pk8和x509.pem檔案,而且都是給apk進行簽名的,那麼keystore檔案和pk8,x509.pem他們之間是不是有什麼聯絡呢?答案是肯定的,網上搜了一下,果然他們之間是可以轉化的,這裡就不在分析如何進行轉化的,網上的例子貌似很多,有專門的的工具可以進行轉化:
那麼到這裡我們就弄清楚了這兩個簽名工具的區別和聯絡。
三、分析Android中籤名流程機制
下面我們開始從原始碼的角度去看看Android中的簽名機制和原理流程
因為網上沒有找到jarsigner的原始碼,但是找到了signapk的原始碼,那麼下面我們就來看看signapk的原始碼吧:
原始碼位置:com/android/signapk/sign.java
通過上面的簽名時我們可以看到,Android簽名apk之後,會有一個META-INF資料夾,這裡有三個檔案:
MANIFEST.MF
CERT.RSA
CERT.SF
下面來看看這三個檔案到底是幹啥的?
1、MANIFEST.MF
我們來看看原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
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函式中,我們看到需要輸入四個引數,然後就做了三件事:
寫MANIFEST.MF
1 2 3 4 5 6 |
//MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
|
在進入方法看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/** 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;
}
|
程式碼邏輯還是很簡單的,主要看那個迴圈的意思:
除了三個檔案(MANIFEST.MF,CERT.RSA,CERT.SF),其他的檔案都會對檔案內容做一次SHA1演算法,就是計算出檔案的摘要資訊,然後用Base64進行編碼即可,下面我們用工具來做個案例看看是不是這樣:
首先安裝工具:HashTab
那下面就開始我們的驗證工作吧:
我們就來驗證一下AndroidManifest.xml檔案,首先在MANIFEST.MF檔案中找到這個條目,記錄SHA1的值
然後我們安裝HashTab之後,找到AndroidManifest.xml檔案,右擊,選擇Hashtab:
複製SHA-1的值:9C64812DE7373B201C294101473636A3697FD73C,到上面的那個Base64轉化網站,轉化一下:
nGSBLec3OyAcKUEBRzY2o2l/1zw=
和MANIFEST.MF中的條目內容一模一樣啦啦
那麼從上面的分析我們就知道了,其實MANIFEST.MF中儲存的是:
逐一遍歷裡面的所有條目,如果是目錄就跳過,如果是一個檔案,就用SHA1(或者SHA256)訊息摘要演算法提取出該檔案的摘要然後進行BASE64編碼後,作為“SHA1-Digest”屬性的值寫入到MANIFEST.MF檔案中的一個塊中。該塊有一個“Name”屬性,其值就是該檔案在apk包中的路徑。
2、下面再來看一下CERT.SF檔案內容
這裡的內容感覺和MANIFEST.MF的內容差不多,來看看程式碼吧:
1 2 3 4 5 6 7 |
//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));
|
進入到writeSignatureFile方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** 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" );
|