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、签名文件和证书
签名文件和证书是成对出现了,二者不可分离,并且我们后面通过源代码能够看到,这两个文件的名字也是一样的,仅仅是后缀名不一样。
事实上数字签名的概念非常easy。
大家知道,要确保可靠通信,必须要解决两个问题:首先,要确定消息的来源确实是其申明的那个人;其次,要保证信息在传递的过程中不被第三方篡改,即使被篡改了。也能够发觉出来。
所谓数字签名,就是为了解决这两个问题而产生的,它是对前面提到的非对称加密技术与数字摘要技术的一个具体的应用。
对于消息的发送者来说,先要生成一对公私钥对。将公钥给消息的接收者。
假设消息的发送者有一天想给消息接收者发消息,在发送的信息中,除了要包括原始的消息外。还要加上另外一段消息。这段消息通过例如以下两步生成:
1)对要发送的原始消息提取消息摘要;
2)对提取的信息摘要用自己的私钥加密。
通过这两步得出的消息。就是所谓的原始信息的数字签名。
而对于信息的接收者来说,他所收到的信息,将包括两个部分,一是原始的消息内容。二是附加的那段数字签名。
他将通过以下三步来验证消息的真伪:
1)对原始消息部分提取消息摘要,注意这里使用的消息摘要算法要和发送方使用的一致。
2)对附加上的那段数字签名,使用预先得到的公钥解密;
3)比較前两步所得到的两段消息是否一致。假设一致,则表明消息确实是期望的发送者发的。且内容没有被篡改过。相反。假设不一致。则表明传送的过程中一定出了问题,消息不可信。
通过这样的所谓的数字签名技术,确实能够有效解决可靠通信的问题。假设原始消息在传送的过程中被篡改了,那么在消息接收者那里。对被篡改的消息提取的摘要肯定和原始的不一样。
并且,由于篡改者没有消息发送方的私钥,即使他能够又一次算出被篡改消息的摘要。也不能伪造出数字签名。
所以。综上所述。数字签名事实上就是仅仅有信息的发送者才干产生的别人无法伪造的一段数字串。这段数字串同一时候也是对信息的发送者发送信息真实性的一个有效证明。
不知道大家有没有注意,前面讲的这样的数字签名方法,有一个前提,就是消息的接收者必须要事先得到正确的公钥。假设一開始公钥就被别人篡改了,那坏人就会被你当成好人,而真正的消息发送者给你发的消息会被你视作无效的。并且,非常多时候根本就不具备事先沟通公钥的信息通道。
那么怎样保证公钥的安全可信呢?这就要靠数字证书来攻克了。
所谓数字证书,一般包括以下一些内容:
证书的公布机构(Issuer)
证书的有效期(Validity)
消息发送方的公钥
证书全部者(Subject)
数字签名所使用的算法
数字签名
能够看出,数字证书事实上也用到了数字签名技术。仅仅只是要签名的内容是消息发送方的公钥,以及一些其它信息。
但与普通数字签名不同的是,数字证书中签名者不是随随便便一个普通的机构,而是要有一定公信力的机构。
这就好像你的大学毕业证书上签名的一般都是德高望重的校长一样。一般来说。这些有公信力机构的根证书已经在设备出厂前预先安装到了你的设备上了。所以,数字证书能够保证数字证书里的公钥确实是这个证书的全部者的,或者证书能够用来确认对方的身份。数字证书主要是用来解决公钥的安全发放问题。
综上所述,总结一下,数字签名和签名验证的大体流程例如以下图所看到的:
3、jarsign和signapk工具
了解到完了签名中的三个文件的知识点之后。以下继续来看看Android中签名的两个工具:jarsign和signapk
关于这两个工具開始的时候非常easy混淆。感觉他们两究竟有什么差别吗?
事实上这两个工具非常好理解。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文件的重要性。说白了就相当于你的银行卡password。
你懂得。
这里我们看到用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的别名输错了。
这个应该和系统默认的签名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
我们来看看源代码:
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
//MANIFEST.MF Manifest manifest = addDigestsToManifest(inputJar); je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); manifest.write(outputJar);在进入方法看看:
/** 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; }代码逻辑还是非常easy的。主要看那个循环的意思:
除了三个文件(MANIFEST.MF,CERT.RSA,CERT.SF),其它的文件都会对文件内容做一次SHA1算法,就是计算出文件的摘要信息,然后用Base64进行编码就可以,以下我们用工具来做个案例看看是不是这样:
首先安装工具:HashTab
然后另一个站点就是在线计算Base64:http://tomeko.net/online_tools/hex_to_base64.php?lang=en
那以下就開始我们的验证工作吧:
我们就来验证一下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的内容几乎相同,来看看代码吧:
//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方法中:
/** 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); }
首先我们能够看到,须要对之前的MANIFEST.MF文件整个内容做一个SHA1放到SHA1-Digest-Manifest字段中:
我们看看出入的manifest变量就是刚刚写入了MANIFEST.MF文件的
这个我们能够验证一下:
然后转化一下
看到了吧,和文件里的值是一样的啦啦
以下我们继续看代码。有一个循环:
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变量。遍历他的条目内容。然后进行SHA算法计算在Base64一下:
事实上就是对MANIFEST.MF文件里的每个条目内容做一次SHA,在保存一下就可以,做个样例验证一下:
用AndroidManifest.xml为例。我们把MANIFEST.MF文件里的条目拷贝保存到txt文档中:
这里须要注意的是。我们保存之后,须要加入两个换行,我们能够在代码中看到逻辑:
然后我们计算txt文档的SHA值:
看到了吧,这里计算的值是一样的啦啦
到这里我们就知道CERT.SF文件做了什么:
1》计算这个MANIFEST.MF文件的总体SHA1值。再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下
2》逐条计算MANIFEST.MF文件里每个块的SHA1,并经过BASE64编码后。记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest
3、最后我们在来看一下CERT.RSA文件
这里我们看到的都是二进制文件。由于RSA文件加密了。所以我们须要用openssl命令才干查看其内容
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text
关于这些信息。能够看以下这张图:
我们来看一下代码:
/** 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); }我们看到,这里会把之前生成的 CERT.SF文件, 用私钥计算出签名, 然后将签名以及包括公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。
四、为何要这么来签名
上面我们就介绍了签名apk之后的三个文件的具体内容。那么以下来总结一下。Android中为何要用这样的方式进行加密签名。这样的方加密是不是最安全的呢?以下我们来分析一下,假设apk文件被篡改后会发生什么。
首先。假设你改变了apk包中的不论什么文件。那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同。于是验证失败,程序就不能安装成功。
其次,假设你对更改的过的文件相应的算出新的摘要值,然后更改MANIFEST.MF文件里面相应的属性值,那么必然与CERT.SF文件里算出的摘要值不一样。照样验证失败。
最后,假设你还不死心,继续计算MANIFEST.MF的摘要值。相应的更改CERT.SF里面的值,那么数字签名值必然与CERT.RSA文件里记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,由于没有数字证书相应的私钥。
所以,假设要又一次打包后的应用程序能再Android设备上安装,必须对其进行重签名。
从上面的分析能够得出。仅仅要改动了Apk中的不论什么内容,就必须又一次签名。不然会提示安装失败,当然这里不会分析,后面一篇文章会注重分析为何会提示安装失败。
五、知识点梳理
1、数据指纹,签名文件,证书文件的含义
1》数据指纹就是对一个数据源做SHA/MD5算法,这个值是唯一的
2》签名文件技术就是:数据指纹+RSA算法
3》证书文件里包括了公钥信息和其它信息
4》在Android签名之后,当中SF就是签名文件。RSA就是证书文件我们能够使用openssl来查看RSA文件里的证书信息和公钥信息
2、我们了解了Android中的签名有两种方式:jarsigner和signapk 这两种方式的差别是:
1》jarsigner签名时,须要的是keystore文件,而signapk签名的时候是pk8,x509.pem文件
2》jarsigner签名之后的SF和RSA文件名称默认是keystore的别名,而signapk签名之后文件名称是固定的:CERT
3》Eclipse中我们在跑Debug程序的时候,默认用的是jarsigner方式签名的,用的也是系统默认的debug.keystore签名文件
4》keystore文件和pk8,x509.pem文件之间能够互相转化
六、思考
我们在分析了签名技术之后,无意中发现一个问题,就是CERT.SF。MANIFEST.MF,这两个文件里的内容的name字段都是apk中的资源名,那么就有一个问题了,假设资源名非常长。并且apk中的资源非常多。那么这两个文件就会非常大。那么这里我们是不是能够优化呢?后面在分析怎样减小apk大小的文章中会继续解说,这里先提出这个问题。
资源下载:http://download.csdn.net/detail/jiangwei0910410003/9377046
总结
上面我们就通过源代码来介绍了Android中的签名过程。整个过程还是非常清楚的,文章写得有点长。假设大家看的有问题的话,记得给我留言。后面我还会再写一篇姊妹篇文章:Android中的签名校验过程具体解释,期待中~~
PS: 关注微信,最新Android技术实时推送