androidv1签名与校验原理分析(全网最全最详细)(代码片段)

潇曜 潇曜     2022-12-09     254

关键词:

【前言】

     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-DigestSHA-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*.DSASIG-* 文件之外都参与摘要签名计算,而且也只是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.5MANIFEST.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-nameks-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.2PKCS#7中包含了X.509证书(密码学里公钥证书的格式标准),X.509证书格式如下
在这里插入图片描述
可以通过openssl以下命令查看CERT.RSA文件中包含的所有x509证书详情

openssl pkcs7 -inform DER -in <*.RSA文件路径> -text -noout -print_certs

显示信息如下:
在这里插入图片描述
3.3PKCS#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, 从pkcs7X.509证书中读取出公钥pk,从pkcs7signerInfo中读取出签名数据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签名源码部分比较绕,所以这里对源码阅读进行一个简要的分析,以便大家快速找到自己想要阅读的部分
在这里插入图片描述

1verifyCertificate里面的包括:方法verifyBytes(校验.SF文件摘要是否跟.RSA文件中记录的摘要值一致)、verify(校验MANIFEST.MF文件的摘要是否.SF文件的记录一致)

2loadCertificates方法主要是校验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️⃣工作原理:&#x 查看详情

全网最全的华为数通思维导图!(代码片段)

...基础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.... 查看详情