关键词:
【前言】
Android Apk V1签名方式是一开始时使用的签名方案,不过V1签名方式也称作
Jar签名
,顾名思义,就是V1签名并不是Android独有的签名方式,而且在Android还没出来时候,Jar 包也是用这种方式进行签名检验的,直到Android 7.0
开始才推出V2签名
,这个就是Android独创的签名方案,签名与校验的效率方面提高很多,后面Android 9.0
又推出了V3签名
,再到Android 11
推出了V4签名方案
一、V1签名过程分析
1、MANIFEST.MF
遍历Apk中除了META-INF目录下以下文件之外
的所有文件,
META-INF/MANIFEST.MF
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
META-INF/SIG-*
并对他们逐一使用SHA1或者SHA256算法
,计算出摘要值,Base64
之后保存到 MANIFEST.MF
文件中,
最终 MANIFEST.MF
文件内容大致如下:
1.1、前面3行是主属性记录
:
Manifest-Version: 1.0
Built-By: Signflinger
Created-By: Android Gradle 4.1.2
1.2、 其中每个文件摘要前的SHA-256-Digest
,这个由签名apk时所用到签名文件的签名算法
以及apk本身适配的最小SDK版本号
共同决定,取值可能是SHA-1-Digest
与 SHA-256-Digest
,下面是签名工具的代码实现部分:
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(PublicKey signingKey, int minSdkVersion) throws InvalidKeyException
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm))
if (minSdkVersion < 18)
return DigestAlgorithm.SHA1;
return DigestAlgorithm.SHA256;
if ("DSA".equalsIgnoreCase(keyAlgorithm))
if (minSdkVersion < 21)
return DigestAlgorithm.SHA1;
return DigestAlgorithm.SHA256;
if ("EC".equalsIgnoreCase(keyAlgorithm))
if (minSdkVersion < 18)
throw new InvalidKeyException("ECDSA signatures only supported for minSdkVersion 18 and higher");
return DigestAlgorithm.SHA256;
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
1.3、上面说到META目录下
除了MENIFEST.MF
、*.SF
、*.RSA
、*.DSA
、SIG-*
文件之外都参与摘要签名计算,而且也只是META根目录的这些文件不参与计算签名, 在META目录的子目录
中的所有文件
都是参与签名的,看下图也可得知:
1.4、遍历对apk中的文件解压
之后,再利用SHA1或者SHA256算法
计算摘要签名,然后Base64
之后保存到MENIFEST.MF
中,主要代码实现逻辑如下:
//解压并更新摘要类
private static class InflateSinkAdapter
implements DataSink, Closeable
private final DataSink mDelegate;
.....
public void consume(byte[] buf, int offset, int length) throws IOException
checkNotClosed();
this.mInflater.setInput(buf, offset, length);
if (this.mOutputBuffer == null)
this.mOutputBuffer = new byte[65536];
while (!this.mInflater.finished())
int outputChunkSize;
try
//对文件进行解压,outputChunkSize为解压之后的大小,mOutputBuffer保存解压之后的数据
outputChunkSize = this.mInflater.inflate(this.mOutputBuffer);
catch (DataFormatException e)
throw new IOException("Failed to inflate data", e);
if (outputChunkSize == 0)
return;
//mDelegate为MessageDigestSink对象,对解压之后的文件进行摘要签名更新
this.mDelegate.consume(this.mOutputBuffer, 0, outputChunkSize);
this.mOutputByteCount += outputChunkSize;
.....
// 摘要数据更新类
public class MessageDigestSink implements DataSink
private final MessageDigest[] mMessageDigests;
public MessageDigestSink(MessageDigest[] digests)
this.mMessageDigests = digests;
public void consume(byte[] buf, int offset, int length)
for (MessageDigest md : this.mMessageDigests)
md.update(buf, offset, length);
public void consume(ByteBuffer buf)
int originalPosition = buf.position();
for (MessageDigest md : this.mMessageDigests)
buf.position(originalPosition);
md.update(buf);
//计算摘要
private static void fulfillInspectInputJarEntryRequest(DataSource lfhSection, LocalFileRecord localFileRecord, ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest) throws IOException, ZipFormatException
//解压本地文件数据出来并放入到MessageDigestSink中
localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
// 计算出文件数据的摘要签名
inspectEntryRequest.done();
1.5、MANIFEST.MF
的行最长只允许70个字符
,这里面包括:Name 与 文件名中间的冒号与空格
(不包括\\r\\n
,加上回车换行符共72个字符
),要是超出70个字符就回车换行
,然后在新行先写入1个空格
,再继续写入剩下的文件名
,代码实现如下:
private static final byte[] CRLF = new byte[]13, 10;
private static final int MAX_LINE_LENGTH = 70;
private static void writeAttribute(OutputStream out, String name, String value) throws IOException
writeLine(out, name + ": " + value);
private static void writeLine(OutputStream out, String line) throws IOException
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
int offset = 0;
int remaining = lineBytes.length;
boolean firstLine = true;
while (remaining > 0)
int chunkLength;
if (firstLine)
//一行最高70个字符,超过70个就换行显示
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
else
//回车换行
out.write(CRLF);
//空格
out.write(32);
//因为这一行多了1个空格,所以最多只能69个字符
chunkLength = Math.min(remaining, 69);
out.write(lineBytes, offset, chunkLength);
offset += chunkLength;
remaining -= chunkLength;
firstLine = false;
//末尾回车换行
out.write(CRLF);
1.6、因为每次写入1个数据块就写入2对回车换行符,所以在MANIFEST.MF末尾
会有2个空行
,下面看看每次写入1个数据块的代码实现:
//写入数据块
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) throws IOException
//写入类似:Name: AndroidManifest.xml\\r\\n
writeAttribute(out, "Name", name);
if (!attributes.isEmpty())
//写入类似:SHA1-Digest: tJkLYKjlAku97m4hDC7yxlJK4XA=\\r\\n
writeAttributes(out, getAttributesSortedByName(attributes));
//写入:\\r\\n
writeSectionDelimiter(out);
// 写入回车换行符
static void writeSectionDelimiter(OutputStream out) throws IOException
out.write(CRLF);
2、CERT.SF
.SF文件名是由签名时候所传入的参数v1-signer-name
、ks-key-alias
或者keystore文件名
所决定,不是固定为CERT.SF,.SF文件的主要作用是对MANIFEST.MF
做校验,防止MANIFEST.MF
的数据被篡改。.SF文件主要保存了MANIFEST.MF整个文件的签名摘要信息
以及每一个数据块的签名摘要信息
,信息如下:
2.1、第一个数据块是.SF文件的主属性信息
:
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: RF3bTyfX9uHZesEx91eumLw7u4EYS1cMc5emxi0npso=
X-Android-APK-Signed: 2, 3
其中,SHA-256-Digest-Manifest
这个属性的值是对MANIFEST.MF整个文件
用SHA-256
散列算法计算出的摘要Base64
的值;
X-Android-APK-Signed
,这个属性指定是否开启比V1更高级的签名方式
,这里值为2,3,说明开启了V2、V3签名,那么应用安装时候,假如跳过V2、V3签名验证(即破坏或者去掉V2、V3签名信息), 直接去验证V1就会抛异常,这个是为了防止降级验证
2.2、有些签名工具还会在.SF主属性中写入SHA-256-Digest-Manifest-Main-Attributes
,这个属性的值是MANIFEST.MF主属性块
的摘要Base64
的值,验证签名的时候会优先验证这一块的摘要,只有验证通过之后才去验证整个MANIFEST.MF文件的数据摘要;对于数据块这个概念定义需要注意一下,下面这样一整块是属于MANIFEST.MF
的一个主属性块,一起参与摘要计算:
Manifest-Version: 1.0
Created-By: 1.8.0_161 (Oracle Corporation)
上面把回车换行符显式表示出来的话,实际是这样:Manifest-Version: 1.0\\r\\nCreated-By: 1.8.0_161 (Oracle Corporation)\\r\\n\\r\\n
,那么对这一整块进行SHA-256算法计算得到值为:
可以用以下代码计算:
public static void main(String[] args)
String data= "Manifest-Version: 1.0\\r\\nCreated-By: 1.8.0_161 (Oracle Corporation)\\r\\n\\r\\n";
MessageDigest md;
try
md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data.getBytes("utf-8"));
String base64Digest = Base64.getEncoder().encodeToString(digest);
System.out.println("\\n******************** 计算结果 ******************** ");
System.out.println(base64Digest);
System.out.println("************************************************** ");
catch (Exception e)
这里一定要注意的是:计算摘要时候,一定要把最后的两个回车换行符一起参与计算,后面各个文件对应的摘要数据块亦是如此,来看看签名工具计算出来记录在.SF文件的数值:
由于一行超过了70
个字符,所以SHA-256-Digest-Manifest-Main-Attributes
这一行换行了,删除空格跟换行之后,跟我们计算出来的值是一致的
2.3、接下来就是对各个文件摘要数据块的进行摘要签名计算,计算方式跟主属性摘要计算一样,比如对于MANIFEST.MF下图这一文件摘要数据块:
字符串表示为:Name: res/drawable-mdpi/ic_currency_mad.png\\r\\nSHA-256-Digest: tFS4pZtxah1Uc84XRqsMhYVcBxN0bdI9PKinhLj79UA=\\r\\n\\r\\n
, 用SHA-256算出摘要再Base64的结果如下:
看看.SF文件对应的值,的确也是一致的
3、CERT.RSA
CERT.RSA文件名也不是固定的,命名规则跟.SF文件一样,而且后缀名也根据不同的签名算法,取不同的后缀名:.DSA
、.RSA
、.EC
3.1、CERT.RSA文件实际上是PKCS#7格式
的数据经过DER规则编码
之后的二进制文件
PKCS#7
,即密码消息语法标准
(Cryptographic Message Syntax Standard),是公钥加密标准
(Public Key Cryptography Standards, PKCS)的1.5版本,数据格式大致如下:
DER
(Distinguished Encoding Rules),即可分辨编码规则
,是ASN.1
标准(Abstract Syntax Notation One,抽象语法标记)的一种编码规则ContentInfo
这个字段,理论上来说是存放待签名内容
,在这里的话,也就是对应.SF文件数据
,但是因为可以直接去读取.SF文件数据来进行签名校验,所以实际上ContentInfo并没有保存.SF文件数据
3.2、PKCS#7
中包含了X.509
证书(密码学里公钥证书的格式标准),X.509证书格式如下
可以通过openssl
以下命令查看CERT.RSA文件中包含的所有x509证书
详情
openssl pkcs7 -inform DER -in <*.RSA文件路径> -text -noout -print_certs
显示信息如下:
3.3、PKCS#7
中包含的签名者信息SignerInfo数据结构如下:
其中,EncryptedDigest
中存储的就是*.SF文件数据
用SHA-1
或者SHA-256
算法算出的摘要值,然后用私钥
签名之后的数据
看看运行时signerInfo对象:
打印出来的数据如下:
二、V1签名校验过程分析
1、先在META-INF
目录下查找后缀名为.DSA
或 .RSA
或 .EC
的签名文件,找到之后,根据签名文件的文件名推出.SF文件
的文件名,比如:找到META-INF目录下文件名为:KK.RSA
的签名文件, 那么,可以推出.SF文件的文件名为:KK.SF
2、读取.RSA
签名文件数据,构造出PKCS#7
格式的对象pkcs7
, 从pkcs7
的X.509
证书中读取出公钥pk
,从pkcs7
的signerInfo
中读取出签名数据encryptedDigest
,然后用公钥pk
对签名数据encryptedDigest
进行解密得到摘要数据digest
, 读取.SF文件
数据然后计算摘要得到摘要数据sfDigest
,最后比对摘要数据sfDigest
与摘要数据digest
是否相等,如果相等,说明.SF文件没有被篡改
,否则签名校验失败
3、假如.SF文件中 Created-By
的属性值不存在:signtool
字符串,同时SHA-256-Digest-Manifest-Main-Attributes
(或SHA-1-Digest-Manifest-Main-Attributes)的属性值存在,那么先校验MANIFEST.MF
的主属性数据块的摘要是否跟SHA-256-Digest-Manifest-Main-Attributes
属性值相等,相等的话才继续进行下一步的校验,否则签名校验失败
4、计算MANIFEST.MF整个文件
的摘要值,跟.SF文件记录的SHA-256-Digest-Manifest-Main-Attributes
对应的值比较,假如相等,那么可以肯定MANIFEST.MF文件没有被篡改
,否则需要进一步对MANIFEST.MF
文件中的每一个数据块
进行计算摘要值,然后跟.SF文件
中记录的摘要值进行比对,如果每一个数据块的摘要值都相等才进行下一步的校验,否则签名校验失败
5、先读取AndroidManifest.xml
文件数据计算出摘要值,跟MANIFEST.MF
中记录的摘要值比对,如果相等,继续遍历所有文件并计算出摘要值跟MANIFEST.MF中记录的摘要值比对,否则签名校验失败
三、V1签名校验过程源码分析
因为V1签名源码部分比较绕,所以这里对源码阅读进行一个简要的分析,以便大家快速找到自己想要阅读的部分
1、verifyCertificate
里面的包括:方法verifyBytes
(校验.SF文件摘要是否跟.RSA文件中记录的摘要值一致)、verify
(校验MANIFEST.MF文件的摘要是否.SF文件的记录一致)
2、loadCertificates
方法主要是校验apk内的文件计算出的摘要值是否跟MANIFEST.MF
中记录的一致,其中,计算摘要的详细实现是在read
方法中,从上图可以看出,read
方法最终会调用MessageDigest#digest
方法计算出文件的摘要值,然后调用verifyMessageDigest
方法比对计算出来的摘要值跟MANIFEST.MF
中记录的是否一致
【扩展问题】
V1签名的主要目的是为了防止apk内的文件被篡改,在整个签名过程中,我们可以看到先对Apk内每个文件计算摘要记录到MANIFEST.MF
中,然后又对MANIFEST.MF整个文件
以及每个数据块
计算摘要记录到.SF
中,最后再对.SF整个文件
计算摘要并用私钥
签名记录到.RSA
中,那么这个过程就会有一个疑问,为啥要多此一举去创建一个.SF
文件呢?直接对MANIFEST.MF整个文件
计算摘要并用私钥签名记录到.RSA
中,不是一样可以达到防止篡改的目的吗?从签名校验的过程中分析可以得知,.SF
存在的意义应该是在对MANIFEST.MF整个文件
的摘要值校验失败时,可以再对MANIFEST.MF
中的每一个数据块
进行摘要计算,要是每一个数据块的摘要校验可以通过,那么签名校验依然是可以通过的。只不过这样的一个校验设计逻辑是基于什么方面的考虑呢?这个不得而知,有知道的小伙伴欢迎告知一二
androidv1,v2,v3签名原理详解
AndroidV1,V2,V3签名原理详解签名校验流程不同的签名版本之间的区别V1签名保护机制V2签名保护机制V3签名保护机制怎样判断使用的是哪种签名参考链接:签名校验流程基础知识:1.数字签名2.数字证书3.对称加密和非对称加... 查看详情
selenium万字长文&&全网最详(上)-王者笔记❤️建议收藏❤️(代码片段)
目录🎅第一部分——初识Selenium!🍏1.Selenium是什么?🍒2.运行环境🍓3.安装1️⃣安装selenium库:2️⃣安装ChromeDriver驱动:🍌4.selenium的作用和工作原理1️⃣作用:2️⃣工作原理: 查看详情
全网最全的华为数通思维导图!(代码片段)
...基础VRP系统管理IPv6基础介绍IPv6路由基础DHCPv6链路聚合VLAN原理和配置GARP和GVRPVLAN间路由无线局域网WLANDHCP原理与配置FTP原理与配置Telnet原理与配置ACL访问控制列表AAA原理与配置GRE原理与配置IPSecVPN原理与配置思维导图来源网络ÿ... 查看详情
java中list集合的三种遍历方式(全网最详)
介绍3种方式遍历list集合1创建一个modelpublicclassNews{privateintid;privateStringtitle;privateStringauthor;publicNews(intid,Stringtitle,Stringauthor){super();this.id=id;this.title=title;this.author=author;}publicint 查看详情
全网最全pcie枚举算法分析(以zynq平台实例讲解)
本篇文章分析PCIe上电是如何枚举的,BAR空间访问在以前文章已经讲解,可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。本文篇幅较长,读者需要耐心阅读。 PCIe上电枚举算法主要是... 查看详情
全网最全pcie枚举算法分析(以zynq平台实例讲解)
本篇文章分析PCIe上电是如何枚举的,BAR空间访问在以前文章已经讲解,可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。本文篇幅较长,读者需要耐心阅读。 PCIe上电枚举算法主要是... 查看详情
全网最全python金融大数据挖掘与分析,基础篇(附源代码,pycharm专业版无限期申请)(代码片段)
个人公众号yk坤帝后台回复python金融基础获取源代码1.pycharm专业版无限期申请1.1Python安装与第一个Python程序1.2Python基础知识1.3Python最重要的三大语句详解1.4函数与模块附源代码1.pycharm专业版无限期申请主要通过pycharm编辑器进行编... 查看详情
全网最全的华为数通思维导图!(代码片段)
...距离矢量路由协议——RIP链路状态协议——OSPFHDLC&PPP原理与应用帧中继原理与配置PPPoENAT网络地址转换交换网络基础STP生成树RSTP原理与配置思维导图来源网络,仅用于交流学习,禁止商用!如有侵权,请联系撤... 查看详情
全网最全原理讲解!java集合常用方法
内容介绍这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮... 查看详情
全网最详bpmn.js教材-事件篇
...pmn.js与后台进行交互,要是对bpmn.js不了解的小伙请移步:《全网最详bpmn.js教材-http请求篇》这一章节要讲解是关于bpmn.js的一些事件,通过学习此章节你可以学习到:很多时候你期望的是在用户在进行不同操作的时候能够监听到他操作... 查看详情
redis面试题x50,全网最全(下)
...众号:程序员小羊目录25、是否使用过Redis集群,集群的原理是什么?26、Redis集群方案什么情况下会导致整个集群不可用?27、Redis支持的Java客户端都有哪些?官方推荐用哪个?28、Jedis与Redisson对比有什么优缺点?29、Redis如何设... 查看详情
androidv1签名和v2签名的区别和注意点
参考技术A为什么要谈这个问题:故事发生的原因:我这边做了正式的签名后(v1和v2同时勾选,产生正式的apk),拿给后台,后台再对我的apk签名再进行处理(截取部分签名后,然后重新签名,打入渠道号)!最后神奇的现象发... 查看详情
androidapk签名打包原理分析apk结构分析
...每个文件的内容、来源、用处3)APK的签名、验证过程4)androidv1、v2、v3、v4签名的异同点5)渠道包的方案迭代,涉及到walle的实现原理6)之前一些实际工作中,遇到问题的经验总结,例如:静默安装的实现方案、增量升级之后app... 查看详情
androidapk签名打包原理分析apk结构分析
...每个文件的内容、来源、用处3)APK的签名、验证过程4)androidv1、v2、v3、v4签名的异同点5)渠道包的方案迭代,涉及到walle的实现原理6)之前一些实际工作中,遇到问题的经验总结,例如:静默安装的实现方案、增量升级之后app... 查看详情
宝藏级全网最全的matplotlib详细教程-数据分析必备手册(4.5万字总结)(代码片段)
...网最全的Matplotlib详细教程(4.5万字总结)1.数据分析中常用图折线图:柱状图:直方图:散点图:饼状图:箱线图:更多参考:2.Matplotlib库安装:基本使用:设置图的信息:设置线条... 查看详情
宝藏级全网最全的matplotlib详细教程-数据分析必备手册(4.5万字总结)(代码片段)
...网最全的Matplotlib详细教程(4.5万字总结)1.数据分析中常用图折线图:柱状图:直方图:散点图:饼状图:箱线图:更多参考:2.Matplotlib库安装:基本使用:设置图的信息:设置线条... 查看详情
数据分析干货全网最全!各行业常见的业务指标整理-线下零售
...场需求的变化也是非常的迅速,所以在零售行业数据分析的场景也是非常的多,人货场的理论大家都多少有所耳闻,我们从人货场的变化也可以看到数据分析的一个趋势变化不同的时期,可能这三者之间的侧重点... 查看详情
宝藏级全网最全的seaborn详细教程-数据分析必备手册(2万字总结)(代码片段)
数据分析必备手册-Seaborn详细教程seaborn库安装:官方文档:关系绘图relplot1.基本使用:2.添加hue参数:3.添加col和row参数:4.指定具体的列:5.绘制折线图:分类绘图1.分类散点图:1.1.stripplot:1.2.... 查看详情