Android V2签名与校验原理分析

本文详细介绍了Android V2签名机制,包括签名过程、签名区块格式、V2签名信息数据格式以及摘要计算过程。V2签名提高了APK的计算速度和完整性校验,对ZIP文件的三个关键部分进行分块并行计算摘要,确保了APK的安全性。同时,文章还阐述了V2签名验证的步骤,包括防回滚保护和签名校验流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【前言】

     V1签名作为一种历史悠久的签名方式,弊端也是比较明显的,一方面由于V1签名是对Apk内的单个文件逐一计算摘要进行签名校验的,所以要是Apk内的文件比较多,计算速度是非常慢的,同时又因为只对单个文件的完整性进行校验,那么对apk压缩包包体进行篡改的话,签名依然还是可以校验通过,完整性的校验工作做得不够到位。到了Android 7.0V2签名方式就应运而生,V2签名一种全文件签名方案,它对压缩包的三大基本组成部分:数据区中央目录记录区中央目录记录结尾区进行分块,每小块 1MB,然后并行计算出每小块的摘要值,最后将计算出来的摘要值拼接起来再一起算出整体的摘要值,并用私钥对其进行签名,计算速度方面相比V1签名有很大的提升,同时完整性校验方面做得更加周全。

一、V2签名过程分析

1、V2签名Apk的结构示意图

在这里插入图片描述

     使用 APK v2签名方案 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于ZIP 中央目录部分之前并紧邻该部分。在APK 签名分块内,v2 签名签名者身份信息会存储在 APK 签名方案 v2 分块中。APK v2签名方案 是在 Android 7.0 (Nougat) 才引入的,为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。
     为了保持与 v1 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”之前并紧邻该部分。该分块包含多个“ID-VALUE”对,所采用的封装方式有助于更轻松地在 APK 中找到该分块,APK 的 v2 签名会存储为一个“ID-VALUE”对,其中 ID 为 0x7109871a

2、APK签名分块(APK Signing Block)格式

在这里插入图片描述

     APK签名分块的前8个字节记录了APK签名分块的大小 size of block(不含自身8字节),其后紧接着键值对数据块,数据块由一个个的键值对块组成。 每个键值对块的开始8字节记录了「键值对的ID」+「键值对的Value」的大小,接下来4字节键值对的ID,后面紧跟着对应的值。 ID = 0x7109871a 的键值对块就是保存V2签名信息的地方。 键值对数据块的后面还有8个字节,也是用于记录「整个APK签名分块」的大小,它的值和最开始的8字节相同。 签名块的末尾是一个魔数magic,也就是APK Sig Block 42的 ASCII 码(小端排序)。
     在解析 APK 时,首先要通过以下方法找到“ZIP 中央目录”的起始位置:在文件末尾找到“ZIP 中央目录结尾”记录,然后从该记录中读取“中央目录”起始偏移量。通过 magic 值,可以快速确定“中央目录”前方可能是“APK 签名分块”。然后,通过 size of block 值,可以高效地找到该分块在文件中的起始位置,在解译该分块时,应忽略 ID 未知的“ID-值”对
【注意】
      由上图分析可知,APK签名分块的大小 size of block占8个字节,表示的是除了上图红框之外的所有数据的大小(即黄色框里面那部分数据的大小),一开始以为 size of block取值范围:0~ 263-1 , 结果看了验证V2签名源码,发现size of block取值范围为:24 ~ (整型最大值-8),即24 ~ (231-1-8)
      构造APK签名区块的代码逻辑如下:

    private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
    public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
          new byte[] {
   
              0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
              0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
          };
    public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
  /**
     * 生成签名区块数据
     * @param apkSignatureSchemeBlockPairs
     * @return
     */
    public static byte[] generateApkSigningBlock(
            List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
   
        // FORMAT:
        // uint64:  size (excluding this field)
        // repeated ID-value pairs:
        //     uint64:           size (excluding this field)
        //     uint32:           ID
        //     (size - 4) bytes: value
        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
        // uint64:  size (same as the one above)
        // uint128: magic

        int blocksSize = 0;
        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
   
            blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value
        }

        int resultSize =
                8 // size
                + blocksSize
                + 8 // size
                + 16 // magic
                ;
        ByteBuffer paddingPair = null;
        //若是大小不是4096的倍数,那么需要增加填充块,填充块没有value
        if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
   
            int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
                    (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
            if (padding < 12) {
     // minimum size of an ID-value pair,键值对最小也得8+4
                padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
            }
            paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
            //填充块键值对的大小
            paddingPair.putLong(padding - 8);
            //ID
            paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
            paddingPair.rewind();
            resultSize += padding;
        }

        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);

        //除了当前记录大小的8字节之外的剩余字节大小
        long blockSizeFieldValue = resultSize - 8L;
        result.putLong(blockSizeFieldValue);

        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
   
            byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
            int apkSignatureSchemeId = schemeBlockPair.getSecond();
            long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length;
            // ID -Value键值端的大小
            result.putLong(pairSizeFieldValue);
            // 4字节的ID,比如:v2签名ID: APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a
            result.putInt(apkSignatureSchemeId);
            // Value数据
            result.put(apkSignatureSchemeBlock);
        }

        if (paddingPair != null) {
   
            result.put(paddingPair);
        }

        //倒数第24字节开始的8个字节,也是写入签名区块的大小
        result.putLong(blockSizeFieldValue);
        // 16字节:APK Sig Block 42的 ASCII 码
        result.put(APK_SIGNING_BLOCK_MAGIC);

        return result.array();
    }

3、V2签名信息数据格式

在这里插入图片描述
     APK 由一个或多个签名者签名,每个签名者均由一个签名密钥来表示。该信息会以“APK 签名方案 v2 分块”的形式存储。对于每个签名者,都会存储以下信息:

  • (签名算法、摘要、签名)元组。摘要会存储起来,以便将签名验证和 APK 内容完整性检查拆开进行。
  • 表示签名者身份的 X.509 证书链。
  • 采用键值对形式的其他属性。

在这里插入图片描述

     对于每位签名者,都会使用收到的列表中支持的签名来验证 APK。签名算法未知的签名会被忽略。如果遇到多个支持的签名,则由每个实现来选择使用哪个签名。这样一来,以后便能够以向后兼容的方式引入安全系数更高的签名方法。建议的方法是验证安全系数最高的签名。

4、APK摘要计算过程

为了保护 APK 内容,APK 包含以下 4 个部分:

  • ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
  • APK 签名分块
  • ZIP 中央目录
  • ZIP 中央目录结尾

在这里插入图片描述
APK 签名方案 v2 负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 签名方案 v2 分块”中的 signed data 分块的完整性第 1、3 和 4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些signed data分块则通过一个或多个签名来保护
在这里插入图片描述
第 1、3 和 4 部分的摘要采用以下计算方式,类似于两级 Merkle 树:

① 拆分块chunk

     将每个部分(即上面标注第1、3、4部分)拆分成多个大小为 1 MB大小的块chunk,最后一个块chunk可能小于1MB。之所以分块,是为了可以通过并行计算摘要以加快计算速度;

② 计算块chunk摘要

     字节 0xa5 + 块的长度(字节数) + 块的内容 拼接起来用对应的摘要算法进行计算出每一块的摘要值;

③ 计算整体摘要

     字节 0x5a + chunk数 + 块的摘要(按块在 APK 中的顺序)拼接起来用对应的摘要算法进行计算出整体的摘要值;

【注意】
     中央目录结尾记录中包含了中央目录的起始偏移量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值