又是过了好长时间,没写文章的双手都有点难受了。今天是圣诞节,仍是得上班。由于前几天有一个以前的同事,在申请微信SDK的时候,遇到签名的问题,问了我一下,结果把我难倒了。。我说Android中的签名你们都会熟悉的,就是为了安全,不让别人修改你的apk,可是咱们真正的有了解多少呢?因此准备两篇文章好好介绍一下Android中签名机制。php
在说道Android签名以前,咱们须要了解的几个知识点java
一、数据摘要(数据指纹)、签名文件,证书文件android
二、jarsign工具签名和signapk工具签名git
三、keystore文件和pk8文件,x509.pem文件的关系算法
四、如何手动的签名apk安全
上面介绍的四个知识点,就是今天介绍的核心,咱们来一一看这些问题。微信
首先来看一下数据摘要,签名文件,证书文件的知识点函数
这个知识点很好理解,百度百科便可,其实他也是一种算法,就是对一个数据源进行一个算法以后获得一个摘要,也叫做数据指纹,不一样的数据源,数据指纹确定不同,就和人同样。工具
消息摘要算法(Message Digest Algorithm)是一种能产生特殊输出格式的算法,其原理是根据必定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就被称做原始数据的消息摘要。
著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的变体。
消息摘要的主要特色有:
1)不管输入的消息有多长,计算出来的消息摘要的长度老是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160比特位的输出。
2)通常来讲(不考虑碰撞的状况下),只要输入的原始数据不一样,对其进行摘要之后产生的消息摘要也必不相同,即便原始数据稍有改变,输出的消息摘要便彻底不一样。可是,相同的输入必会产生相同的输出。
3)具备不可逆性,即只能进行正向的信息摘要,而没法从摘要中恢复出任何的原始消息。
优化
签名文件和证书是成对出现了,两者不可分离,并且咱们后面经过源码能够看到,这两个文件的名字也是同样的,只是后缀名不同。
其实数字签名的概念很简单。你们知道,要确保可靠通讯,必需要解决两个问题:首先,要肯定消息的来源确实是其申明的那我的;其次,要保证信息在传递的过程当中不被第三方篡改,即便被篡改了,也能够发觉出来。
所谓数字签名,就是为了解决这两个问题而产生的,它是对前面提到的非对称加密技术与数字摘要技术的一个具体的应用。
对于消息的发送者来讲,先要生成一对公私钥对,将公钥给消息的接收者。
若是消息的发送者有一天想给消息接收者发消息,在发送的信息中,除了要包含原始的消息外,还要加上另一段消息。这段消息经过以下两步生成:
1)对要发送的原始消息提取消息摘要;
2)对提取的信息摘要用本身的私钥加密。
经过这两步得出的消息,就是所谓的原始信息的数字签名。
而对于信息的接收者来讲,他所收到的信息,将包含两个部分,一是原始的消息内容,二是附加的那段数字签名。他将经过如下三步来验证消息的真伪:
1)对原始消息部分提取消息摘要,注意这里使用的消息摘要算法要和发送方使用的一致;
2)对附加上的那段数字签名,使用预先获得的公钥解密;
3)比较前两步所获得的两段消息是否一致。若是一致,则代表消息确实是指望的发送者发的,且内容没有被篡改过;相反,若是不一致,则代表传送的过程当中必定出了问题,消息不可信。
经过这种所谓的数字签名技术,确实能够有效解决可靠通讯的问题。若是原始消息在传送的过程当中被篡改了,那么在消息接收者那里,对被篡改的消息提取的摘要确定和原始的不同。而且,因为篡改者没有消息发送方的私钥,即便他能够从新算出被篡改消息的摘要,也不能伪造出数字签名。
因此,综上所述,数字签名其实就是只有信息的发送者才能产生的别人没法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证实。
不知道你们有没有注意,前面讲的这种数字签名方法,有一个前提,就是消息的接收者必需要事先获得正确的公钥。若是一开始公钥就被别人篡改了,那坏人就会被你当成好人,而真正的消息发送者给你发的消息会被你视做无效的。并且,不少时候根本就不具有事先沟通公钥的信息通道。那么如何保证公钥的安全可信呢?这就要靠数字证书来解决了。
所谓数字证书,通常包含如下一些内容:
证书的发布机构(Issuer)
证书的有效期(Validity)
消息发送方的公钥
证书全部者(Subject)
数字签名所使用的算法
数字签名
能够看出,数字证书其实也用到了数字签名技术。只不过要签名的内容是消息发送方的公钥,以及一些其它信息。但与普通数字签名不一样的是,数字证书中签名者不是随随便便一个普通的机构,而是要有必定公信力的机构。这就好像你的大学毕业证书上签名的通常都是德高望重的校长同样。通常来讲,这些有公信力机构的根证书已经在设备出厂前预先安装到了你的设备上了。因此,数字证书能够保证数字证书里的公钥确实是这个证书的全部者的,或者证书能够用来确认对方的身份。数字证书主要是用来解决公钥的安全发放问题。
综上所述,总结一下,数字签名和签名验证的大致流程以下图所示:
了解到完了签名中的三个文件的知识点以后,下面继续来看看Android中签名的两个工具:jarsign和signapk
关于这两个工具开始的时候很容易混淆,感受他们两到底有什么区别吗?
其实这两个工具很好理解,jarsign是Java本生自带的一个工具,他能够对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不同,这个就要引出第三个问题了。
咱们上面了解到了jarsign和signapk两个工具均可以进行Android中的签名,那么他们的区别在于签名时使用的文件不同
jarsign工具签名时使用的是keystore文件
signapk工具签名时使用的是pk8,x509.pem文件
其中咱们在使用Eclipse工具写程序的时候,出Debug包的时候,默认用的是jarsign工具进行签名的,并且Eclipse中有一个默认签名文件:
咱们能够看到这个默认签名的keystore文件,固然咱们能够选择咱们本身指定的keystore文件。
这里还有一个知识点:
咱们看到上面有MD5和SHA1的摘要,这个就是keystore文件中私钥的数据摘要,这个信息也是咱们在申请不少开发平台帐号的时候须要填入的信息,好比申请百度地图,微信SDK等,会须要填写应用的MD5或者是SHA1信息。
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的别名输错了。
注意: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中的签名机制和原理流程
由于网上没有找到jarsigner的源码,可是找到了signapk的源码,那么下面咱们就来看看signapk的源码吧:
源码位置:com/android/signapk/sign.java
经过上面的签名时咱们能够看到,Android签名apk以后,会有一个META-INF文件夹,这里有三个文件:
MANIFEST.MF
CERT.RSA
CERT.SF
下面来看看这三个文件究竟是干啥的?
咱们来看看源码:
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; }代码逻辑仍是很简单的,主要看那个循环的意思:
除了三个文件(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包中的路径。
这里的内容感受和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
这里咱们看到的都是二进制文件,由于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》数据指纹就是对一个数据源作SHA/MD5算法,这个值是惟一的
2》签名文件技术就是:数据指纹+RSA算法
3》证书文件中包含了公钥信息和其余信息
4》在Android签名以后,其中SF就是签名文件,RSA就是证书文件咱们可使用openssl来查看RSA文件中的证书信息和公钥信息
二、咱们了解了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技术实时推送