天天看点

APK签名机制之——JAR签名机制详解

转载请注明出处:https://blog.csdn.net/zwjemperor/article/details/80877305 github:https://github.com/rushgit/zhongwenjun.github.com

在APK签名机制原理详解中我们已经了解了APK签名和校验的基本过程,这一篇我们来分析JAR签名机制。JAR签名对对jar包进行签名的一种机制,由于jar包apk本质上都是zip包,所以可以应用到对apk的签名。本文从JAR签名结构、签名过程,再到签名校验的源码分析,全方面来分析Android中JAR签名及校验的机制。

1. 签名过程

APK签名机制之——JAR签名机制详解

通过解压工具打开apk文件,会发现有一个META-INF目录,该目录中有3个文件,这3个文件是签名以后生成的,显然与签名相关,我们依次看这几个文件中的内容。

这个文件列出了apk中所有的文件,以及它们的摘要,摘要字符串是通过base64编码的,通过计算来验证下:

先计算出res/drawable-hdpi-v4/abc_list_longpressed_holo.9.png的sha1值。

APK签名机制之——JAR签名机制详解

计算出的sha1值是经过16进行编码的,再把它转成base64编码,可以通过在线工具进行转换:tomeko.net

APK签名机制之——JAR签名机制详解

可以看到转换过后的base64值和MANIFEST.MF文件中内容是一样的。

SF文件的内容和MF比较相似,同样包含了apk所有文件的摘要,不同的是:

SF文件在主属性中记录了整个MF文件的摘要(SHA1-Digest-Manifest)

SF文件其余部分记录的是MF相应条目的摘要,也就说对MF文件相应条目再次进行了摘要计算。

我们再来验证下,首先计算出MANIFEST.MF文件的sha1值,再转换base64编码:

APK签名机制之——JAR签名机制详解
APK签名机制之——JAR签名机制详解

再来验证res/drawable-hdpi-v4/abc_list_longpressed_holo.9.png

这里要注意下,.MF文件是以空行分隔的。计算.MF各条目摘要时需要再加一个换行符,因为空行还有一个换行符(具体可参考apksigner源码)。我们把abc_list_longpressed_holo.9.png条目保存到一个新文件中,先计算这个条目的sha1值:

APK签名机制之——JAR签名机制详解

再把sha1值转为base64编码:

APK签名机制之——JAR签名机制详解

cert.rsa中的是二进行内容,里面保存了签名者的证书信息,以及对cert.sf文件的签名。具体证书包含的内容已经在APK签名机制原理详解中作了说明,这里不再重复介绍。

具体签名过程如下:

APK签名机制之——JAR签名机制详解

具体过程可参考apksigner源码

2. 校验过程

上面说的是签名过程,接下来看apk安装过程是怎样进行签名校验的。校验过程和签名过程刚好相反:

首先校验cert.sf文件的签名

计算cert.sf文件的摘要,与通过签名者公钥解密签名得到的摘要进行对比,如果一致则进入下一步;

校验manifest.mf文件的完整性

计算manifest.mf文件的摘要,与cert.sf主属性中记录的摘要进行对比,如一致则逐一校验mf文件各个条目的完整性;

校验apk中每个文件的完整性

逐一计算apk中每个文件(META-INF目录除外)的摘要,与mf中的记录进行对比,如全部一致,刚校验通过;

校验签名的一致性

如果是升级安装,还需校验证书签名是否与已安装app一致。

以上步骤需要全部通过才算签名校验通过,任何一步失败都将导致校验失败。这个过程能保证apk不可被篡改吗?我们来看看篡改apk内容会发生什么:

篡改apk内容

校验apk中每个文件的完整性时失败;如果是添加新文件,因为此文件的hash值在.mf和.sf中无记录,同样校验失败;

篡改apk内容,同时篡改manifest.mf文件相应的摘要

校验manifest.mf文件的摘要会失败;

篡改apk内容,同时篡改manifest.mf文件相应的摘要,以及cert.sf文件的内容

校验cert.sf文件的签名会失败;

把apk内容和签名信息一同全部篡改

这相当于对apk进行了重新签名,在此apk没有安装到系统中的情况下,是可以正常安装的,这相当于是一个新的app;但如果进行覆盖安装,则证书不一证,安装失败。

从这里可以看出只要篡改了apk中的任何内容,都会使得签名校验失败。

3. 签名校验代码分析

前面介绍了签名和校验的整体流程,现在来看校验过程的代码实现。安装apk的入口在frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java的installPackageLI方法。(注:这里参考的是5.1.1版本的源码)

在这方法中构造了一个PackageParser对象,这个类是用来解析apk文件的,具体的签名校验在这个对象的collectCertificates方法中。

这个方法中创建了一个StrictJarFile对象,在StrictJarFile的构造方法中完成了CERT.SF文件的签名校验和MANIFEST.MF文件的hash校验,校验的结果保存在了isSigned成员变量中。

StrictJarFile中构造了Manifest和JarVerifier对象,具体校验过程是中在readCertificates方法中实现的,校验成功后将证书保存到了certificates集合中。来看readCertificates方法的具体实现:

如果meta条目为空,则直接返回false;如果存在,则遍历找到签名块文件(.DSA/.RSA/.EC),依次进行校验。apk签名通常使用RSA算法,所以找到的是.RSA文件,从这里可以看出,.RSA文件名并不是固定的,校验过程中是通常后缀查找的。这里是一个while循环,从这里可以看出,是可以有多个签名者对apk进行签名的,readCertificates会依次对每一个签名进行校验。

继续看verifyCertificate的实现:

首先通过.RSA文件名找到.SF文件,然后通过JarUtils类的verifySignature方法校验.SF文件签名。校验过后把相应证书保存到了certificates成员变量。

verifySignature方法入参分别是.SF文件和签名块(.RSA文件)的二进制流,这个方法中计算了.SF文件的摘要,对其签名做了校验。

继续看verifyCertificate方法的后半段实现:

verifyCertificate方法的后半段做了2个事情:第一件是找到.SF文件的SHA1-Digest-Manifest属性值,校验.MF文件hash的正确性;第二件是针对.SF文件中的每个条目,校验.MF文件相应条目hash的正确性。具体校验的工作在verify方法中完成:

这里进行了一次for循环来找到.SF文件使用的hash算法,校验的过程很简单,用相应hash算法计算.MF文件相应条目的摘要,比较是否一致即可。至此完成了对.MF文件hash及.MF文件各条目hash的校验。

继续看的PackageParser collectCertificates方法:

在通过StrictJarFile构造方法完成.SF和.MF文件的校验之后,首先查找AndroidManifest.xml文件是否存在,不存在则直接抛出异常;然后遍历apk中的条个文件,把除META-INF目录之外的文件加入toVerify集合,然后对toVerity集合中的每一个文件进行校验。先来看loadCertificates方法:

再看StrictJarFile的getInputStream方法:

在getInputStream判断了isSigned,这个字段在StrictJarFile构造方法中校验.SF文件和.MF后赋值为true。通过JarVerifier的initEntry方法拿到了VerifierEntry对象,再来看initEntry的实现:

这个方法做了2件事情:一是遍历.SF文件中已经过签名校验的条目(signatures map是在verifyCertificate中校验.SF文件后保存的),查找是否存在方法入参指定的文件名,不存在则说明是新增文件,直接返回null;第二件事是构造VerifierEntry对象,参数分别是 文件名、hash算法、.MF文件中相应文件名对应的hash值、证书链、已签名的文件列表。

getInputStream在创建VerifierEntry对象后,进行了一次封装,返回了JarFile.JarFileInputStream对象。再回头来看PackageParser的loadCertificates中调用的readFullyIgnoringContents方法:

这个方法看上去只是读取文件流,但实际上in是JarFileInputStream,来看JarFileInputStream中read方法的实现:

可以看到,read方法中调用了VerifierEntry的verify方法,最终在verify方法中完成了apk中相应文件的hash校验,也就比较apk中各文件的hash与.MF文件中对应的值是否一致。

再回头来看collectCertificates方法的剩余部分:

遍历toVerify集合时,如果loadCertificates返回null,说明该文件是在对apk签名过后新增的文件,抛出异常。紧接着后面再次作了校验,对比后续文件的证书签名和第一个文件的证书签名是否一致,如有不一致仍然抛出异常。

到这一步为止,签名校验的工作基本就结束了,PackageManagerService.java的installPackageLI方法还有一步是针对升级安装的场景,校验证书公钥是否一致。

好了,到这里签名校验的代码介绍就结束了,代码比较乱,梳理一下时序图:

APK签名机制之——JAR签名机制详解

4. JAR签名机制的劣势

从Android 7.0开始,Android支持了一套全新的V2签名机制,为什么要推出新的签名机制呢?通过前面的分析,可以发现JAR签名有两个地方可以改进:

签名校验速度慢

校验过程中需要对apk中所有文件进行摘要计算,在apk资源很多、性能较差的机器上签名校验会花费较长时间,导致安装速度慢;

完整性保障不够

META-INF目录用来存放签名,自然此目录本身是不计入签名校验过程的,可以随意在这个目录中添加文件,比如一些快速批量打包方案就选择在这个目录中添加渠道文件。

为了解决这两个问题,Android 7.0推出了全新的签名方案V2,关于V2签名机制的详解参见下一篇文章Apk签名机制之——V2签名机制详解