Android签名与渠道包制作-V2/V3签名原理

作者: 嘉伟咯 | 来源:发表于2021-04-07 20:45 被阅读0次

    系列文章:

    正如上一篇文章说的,V1版本的签名机制漏洞在于它没有给整个apk包做校验,而且校验的时候需要解压。V2版本的签名机制就是为了解决这两个问题而出现的。

    zip包文件格式

    为了了解V2版本的签名原理,我们需要更加深入的了解下zip包的文件格式。由于zip的解析是从后往前的,大体格式如下:

    1.png

    eocd的倒数第三部分[offset of start of central directory with respect to the starting disk number]标记了central directory的偏移:

    End of central directory record:
    
            end of central dir signature    4 bytes  (0x06054b50)
            number of this disk             2 bytes
            number of the disk with the
            start of the central directory  2 bytes
            total number of entries in the
            central directory on this disk  2 bytes
            total number of entries in
            the central directory           2 bytes
            size of the central directory   4 bytes
            offset of start of central
            directory with respect to
            the starting disk number        4 bytes
            .ZIP file comment length        2 bytes
            .ZIP file comment       (variable size)
    

    central directory

    我们直接以一个例子来说明:

    2.png

    由于zip包是小端序号,所以实际的值应该是0x00149928,这个地址就代表着central directory的起始地址,我们对比central directory的文件结构:

     Central directory structure:
    
        [file header 1]
        .
        .
        . 
        [file header n]
        [digital signature] 
    
        File header:
    
          central file header signature   4 bytes  (0x02014b50)
          version made by                 2 bytes
          version needed to extract       2 bytes
          general purpose bit flag        2 bytes
          compression method              2 bytes
          last mod file time              2 bytes
          last mod file date              2 bytes
          crc-32                          4 bytes
          compressed size                 4 bytes
          uncompressed size               4 bytes
          file name length                2 bytes
          extra field length              2 bytes
          file comment length             2 bytes
          disk number start               2 bytes
          internal file attributes        2 bytes
          external file attributes        4 bytes
          relative offset of local header 4 bytes
    
          file name (variable size)
          extra field (variable size)
          file comment (variable size)
    
        Digital signature:
    
          header signature                4 bytes  (0x05054b50)
          size of data                    2 bytes
          signature data (variable size)
    

    一堆的文件头,和一个签名。我们在zip包中找到0x00149928这个位置:

    3.png

    根据上面的格式定义将对应的数据列举出来:

    地址 长度 内容 小端序实际值
    0x00149928 4 bytes central file header signature(0x02014b50) 0x504B0102 0x02014B50
    0x0014992C 2 bytes version made by 0x0000 0x0000
    0x0014992E 2 bytes version needed to extract 0x0000 0x0000
    0x00149930 2 bytes general purpose bit flag 0x0000 0x0000
    0x00149932 2 bytes compression method 0x0800 0x0008
    0x00149934 2 bytes last mod file time 0x0000 0x0000
    0x00149936 2 bytes last mod file date 0x0000 0x0000
    0x00149938 4 bytes crc-32 0x39B6CD57 0x57CDB639
    0x0014993C 4 bytes compressed size 0x12030000 0x00000312
    0x00149940 4 bytes uncompressed size 0x98080000 0x00000898
    0x00149944 2 bytes file name length 0x1300 0x0013
    0x00149946 2 bytes extra field length 0x0000 0x0000
    0x00149948 2 bytes file comment length 0x0000 0x0000
    0x0014994A 2 bytes disk number start 0x0000 0x0000
    0x0014994C 2 bytes internal file attributes 0x0000 0x0000
    0x0014994E 4 bytes external file attributes 0x00000000 0x00000000
    0x00149952 4 bytes relative offset of local header 0x00000000 0x00000000
    0x00149956 variable size
    (0x0013==19)
    file name 0x41 0x6E 0x64 0x72 0x6F 0x69 0x64 0x4D 0x61 0x6E 0x69 0x66 0x65 0x73 0x74 0x2E 0x78 0x6D 0x6C ASCII码的值为:AndroidManifest.xml
    - variable size(0) extra field (空) (空)
    - variable size(0) file comment (空) (空)

    所以我们找到第一个文件AndroidManifest.xml的[relative offset of local header]为0x00000000,即local file header 1的地址是0x00000000。

    Local file header

    0x00000000的内容如下:

    4.png

    Local file header的格式如下:

    Local file header:
    
            local file header signature     4 bytes  (0x04034b50)
            version needed to extract       2 bytes
            general purpose bit flag        2 bytes
            compression method              2 bytes
            last mod file time              2 bytes
            last mod file date              2 bytes
            crc-32                          4 bytes
            compressed size                 4 bytes
            uncompressed size               4 bytes
            file name length                2 bytes
            extra field length              2 bytes
    
            file name (variable size)
            extra field (variable size)
    

    根据上面的格式定义将对应的数据列举出来:

    地址 长度 内容 小端序实际值
    0x00000000 4 bytes local file header signature(0x04034b50) 0x504B0304 0x04034B50
    0x00000004 2 bytes version needed to extract 0x0000 0x0000
    0x00000006 2 bytes general purpose bit flag 0x0000 0x0000
    0x00000008 2 bytes compression method 0x0800 0x0008
    0x0000000A 2 bytes last mod file time 0x0000 0x0000
    0x0000000C 2 bytes last mod file date 0x0000 0x0000
    0x0000001E 4 bytes crc-32 0x39B6CD57 0x57CDB639
    0x00000012 4 bytes compressed size 0x12030000 0x00000312
    0x00000016 4 bytes uncompressed size 0x98080000 0x00000898
    0x0000001A 2 bytes file name length 0x1300 0x0013
    0x0000001C 2 bytes extra field length 0x0000 0x0000
    0x0000001E variable size
    (0x0013==19)
    file name 0x41 0x6E 0x64 0x72 0x6F 0x69 0x64 0x4D 0x61 0x6E 0x69 0x66 0x65 0x73 0x74 0x2E 0x78 0x6D 0x6C ASCII码的值为:AndroidManifest.xml
    - variable size(0) extra field (空) (空)

    Local file header后面跟着的就是压缩后的文件数据,这块我们就不再深入了解了。从上面的解析我们可以了解到,zip包的解析其实是从后往前的。

    V2签名原理

    了解完zip包的格式之后,就很容易理解V2签名的原理了。V2签名实际上是在apk的[central directory]前面插入一个apk签名块:

    5.png

    也就是说在eocd读取[offset of start of central directory with respect to the starting disk number]这个地址往前读就是APK签名块了。

    我们来看看这个APK签名块的格式:

    6.png

    由于是往前读,所以结尾16字节是一个用于识别的魔数(字符串"APK Sig Block 42"),再往前是签名块的长度,继续往前是一系列的带长度前缀的id-value键值对,最前面又是签名块的长度。

    我们直接找一个V2签名的apk来分析下:

    7.png

    同样先找到central directory的地址偏移0x00142174:

    8.png

    同样在该地址可以看到0x02014B50这个Central directory的魔数,而往前的16个字节就是字符串”APK Sig Block 42“的ASCII码。继续往前的8个字节则是APK签名块的长度0xFF8。我们用于是我们可以计算出第一个部分的地址:

    0x00142174 - 0xFF8 - 0x8 = 0x00141174
    

    再减去8个字节是因为APK签名块长度不包括第一个部分自身的8个字节。然后我们找到这个地址可以看到值是0x00000000 00000FF8:

    9.png

    根据APK签名块的格式我们知道往后便是第一个id-value键值对。他的长度是0x00000000 0000005F3,而id是0x7109871A。这个id的键值对被命名为"APK 签名方案 v2 分块",里面保存的就是签名的校验数据。

    摘要计算

    校验数据的话首先要考虑的就是摘要算法,例如V1版本将每个原始文件用sha算法算出摘要之后用MANIFEST.MF一个个保存起来。而V2版本考虑了整个apk的校验,所以它并不去计算每个原始文件的摘要,而是计算整个apk的摘要。

    为了加速运算,首先将apk按1m大小分割成若干块,分别计算这些块的摘要,再将这些摘要组合起来计算一次摘要,就得到了整个apk的摘要。并将其放入id为0x7109871A的"APK 签名方案 v2 分块"中:

    10.png

    光讲和看图可能理解还不是特别深入,我们直接干ApkSignerV2的源码:

    private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
    
    private static Map<Integer, byte[]> computeContentDigests(Set<Integer> digestAlgorithms, ByteBuffer[] contents) throws DigestException {
      // 按1M大小分块,计算分块数量
      int chunkCount = 0;
      for (ByteBuffer input : contents) {
          chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
      }
    
      // 可能使用多种算法进行摘要计算
      // 每种算法都会计算所有分块的摘要然后组合起来,再计算一次摘要
      // 这里先创建用于组合的buffer
      final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
      for (int digestAlgorithm : digestAlgorithms) {
          // 获取摘要算法计算结果的大小
          int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
          // 前5个字节是0x5a和4个字节的块数量,后面是各个块的摘要直接连接组合
          byte[] concatenationOfChunkCountAndChunkDigests = new byte[5 + chunkCount * digestOutputSizeBytes];
          // 设置第0个字节为0x5a
          concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
          // 设置第1个字节开始的四个字节为块数量
          setUnsignedInt32LittleEngian(chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
          // 将buffer放入map中
          digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
      }
    
      // 各个分块的摘要计算也是类似的
      // 需要在摘要前面添加五个字节: 0x5a + 块长度
      int chunkIndex = 0;
      byte[] chunkContentPrefix = new byte[5];
      chunkContentPrefix[0] = (byte) 0xa5;
    
      for (ByteBuffer input : contents) {
          while (input.hasRemaining()) {
              // 读取分块
              int chunkSize = Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
              final ByteBuffer chunk = getByteBuffer(input, chunkSize);
    
              // 使用各种算法计算分块的摘要
              for (int digestAlgorithm : digestAlgorithms) {
                  //创建摘要算法实例
                  String jcaAlgorithmName =
                          getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
                  MessageDigest md;
                  try {
                      md = MessageDigest.getInstance(jcaAlgorithmName);
                  } catch (NoSuchAlgorithmException e) {
                      throw new DigestException(
                              jcaAlgorithmName + " MessageDigest not supported", e);
                  }
                  // 这个clear并不会将内容清空,仅仅只是是将内部的指针回到position 0
                  chunk.clear();
    
                  //在0x5a后面放入块的大小
                  setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
    
                  //计算块的摘要
                  md.update(chunkContentPrefix);
                  md.update(chunk);
    
                  // 将计算到的分块摘要放入前面为每种算法创建的buffer中组合起来
                  byte[] concatenationOfChunkCountAndChunkDigests =
                          digestsOfChunks.get(digestAlgorithm);
                  int expectedDigestSizeBytes =
                          getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
                  int actualDigestSizeBytes =
                          md.digest(
                                  concatenationOfChunkCountAndChunkDigests,
                                  5 + chunkIndex * expectedDigestSizeBytes,
                                  expectedDigestSizeBytes);
                  if (actualDigestSizeBytes != expectedDigestSizeBytes) {
                      throw new DigestException(
                              "Unexpected output size of " + md.getAlgorithm()
                                      + " digest: " + actualDigestSizeBytes);
                  }
              }
              chunkIndex++;
          }
      }
    
      // 遍历算法,计算分块摘要组合起来之后的总摘要
      Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
      for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
          int digestAlgorithm = entry.getKey();
          byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
          String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
          MessageDigest md;
          try {
              md = MessageDigest.getInstance(jcaAlgorithmName);
          } catch (NoSuchAlgorithmException e) {
              throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
          }
          result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
      }
      return result;
    }
    

    可以从源码看到计算的流程大概有三步:

    1. 将整个apk按1M大小分块
    2. 用多个摘要算法去计算 "0x5a + 分块长度 + 分块内容" 的摘要
    3. 用多个摘要算法计算 "0x5a + 分块数量 + 各个分块摘要" 的总摘要

    虽然在签名的时候没有使用并行计算,但是实际上各个分块的摘要是独立的,在需要的时候完全可以使用并发计算去加速优化。

    摘要签名

    为了防止攻击者在修改apk之后同步修改摘要,V2签名还会使用签名私钥对上面计算出来的摘要进行签名:

    private static byte[] generateSignerBlock(
          SignerConfig signerConfig,
          Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
      if (signerConfig.certificates.isEmpty()) {
          throw new SignatureException("No certificates configured for signer");
      }
      // 先将公钥保存下来用于
      // 1. 签名之后的验证
      // 2. 写入"APK 签名方案 v2 分块"用于安装时候验证签名
      PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
      byte[] encodedPublicKey = encodePublicKey(publicKey);
    
      // 初始化签名数据
      // 主要是创建<摘要算法id,apk摘要>键值对的列表
      V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
      try {
          signedData.certificates = encodeCertificates(signerConfig.certificates);
      } catch (CertificateEncodingException e) {
          throw new SignatureException("Failed to encode certificates", e);
      }
      List<Pair<Integer, byte[]>> digests =
              new ArrayList<>(signerConfig.signatureAlgorithms.size());
      for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
          int contentDigestAlgorithm =
                  getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
          byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
          if (contentDigest == null) {
              throw new RuntimeException(
                      getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
                      + " content digest for "
                      + getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
                      + " not computed");
          }
          digests.add(Pair.create(signatureAlgorithm, contentDigest));
      }
      signedData.digests = digests;
    
      // 将上面得到的signedData放入signer中用于计算签名
      V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
      // FORMAT:
      // * length-prefixed sequence of length-prefixed digests:
      //   * uint32: signature algorithm ID
      //   * length-prefixed bytes: digest of contents
      // * length-prefixed sequence of certificates:
      //   * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
      // * length-prefixed sequence of length-prefixed additional attributes:
      //   * uint32: ID
      //   * (length - 4) bytes: value
      signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
          encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
          encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
          // additional attributes
          new byte[0],
      });
    
      // 保存公钥
      signer.publicKey = encodedPublicKey;
    
      // 计算各个摘要算法获取的摘要的签名
      signer.signatures = new ArrayList<>();
      for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
          Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
                  getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
          String jcaSignatureAlgorithm = signatureParams.getFirst();
          AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
          byte[] signatureBytes;
    
          // 获取签名算法使用私钥进行签名
          try {
              Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
              signature.initSign(signerConfig.privateKey);
              if (jcaSignatureAlgorithmParams != null) {
                  signature.setParameter(jcaSignatureAlgorithmParams);
              }
              signature.update(signer.signedData);
              signatureBytes = signature.sign();
          } catch (InvalidKeyException e) {
              throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
          } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
                  | SignatureException e) {
              throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
          }
    
          // 使用公钥尝试是否能够正确验证签名
          try {
              Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
              signature.initVerify(publicKey);
              if (jcaSignatureAlgorithmParams != null) {
                  signature.setParameter(jcaSignatureAlgorithmParams);
              }
              signature.update(signer.signedData);
              if (!signature.verify(signatureBytes)) {
                  throw new SignatureException("Signature did not verify");
              }
          } catch (InvalidKeyException e) {
              throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
                      + " signature using public key from certificate", e);
          } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
                  | SignatureException e) {
              throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
                      + " signature using public key from certificate", e);
          }
    
          // 将签名加入签名数据
          signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
      }
    
      // 生成签名二进制数据
      // FORMAT:
      // * length-prefixed signed data
      // * length-prefixed sequence of length-prefixed signatures:
      //   * uint32: signature algorithm ID
      //   * length-prefixed bytes: signature of signed data
      // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
      return encodeAsSequenceOfLengthPrefixedElements(
              new byte[][] {
                  signer.signedData,
                  encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
                          signer.signatures),
                  signer.publicKey,
              });
    }
    

    最后会将计算得到的摘要、摘要签名、公钥、算法信息等数据写入刚刚说的的id为0x7109871A的"APK 签名方案 v2 分块"中,于是在安装apk的时候就能使用这些数据去检查apk是否被修改了:

    11.png

    防回滚保护

    由于需要在Android 7.0之后才支持V2版本的签名,为了兼容低版本的安卓机器,一般情况下我们会同时使用V1和V2版本的签名。但由于V2版本插入apk中间的"APK签名块"是独立于zip格式存在的,攻击者其实可以直接将其直接删掉,使得apk降级回V1。

    而高版本的安卓系统为了兼容旧的apk,也会在找不到Apk签名块的情况下使用V1签名去验证。

    谷歌为了防止这种恶意操作规定:

    同时包含V1和V2签名的CERT.SF文件会加入这样一个属性:

    X-Android-APK-Signed: 2
    

    在Android 7.0之后读取到这个属性的时候就会强制使用V2版本的签名检查机制而不走V1版本的。

    V3签名原理

    由于生成签名的时,可以指定一个有效时间,这个时间默认为 25 年,如果过了这个时间可能会出现签名失效不能再安装的情况。

    说可能是因为网上有人实际验证过,有些机器是没有做这个检查的:

    ==但是,我实际测试了下官方模拟器、小米、vivo、华为荣耀,签名已失效依然可以正常安装。== 网上千篇一律都说失效签名无法安装,不知道他们有没有实际测过。咨询了厂商的开发者,目前只收到了vivo的回复,说是因为手机时间可以随意调,所以这个检验没有任何意义,他们废弃掉了,其他厂商不知道是不是也出于这个原因。

    但是为了防止的确有公司被收购等这样那样的原因需要更换签名,安卓9.0之后提供了V3版本的签名机制。

    V3版本的机制原理是在APK签名块里面新增了一个id为0xF05368C0的键值对,它的格式也和V2版本id为0x7109871A的"APK 签名方案 v2 分块"基本相同,只不过增加了attr块,里面保存了多个level的证书信息。(由于它们的id不一样,所以在V2+V3同时签名的情况下,APK签名块会同时有这两个id的键值对)

    我从这位博主的文章中看到了这附两幅图,能够很形象的解释V2和V3签名间的差异:

    12.png

    在安装的时候会使用旧的证书去验证新证书是否有效。如果当前已经安装的apk的证书在level证书链上,就能逐步完后验证更新的证书的有效性

    13.png

    证书链验证的核心代码如下:

    // frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
    private static VerifiedProofOfRotation verifyProofOfRotationStruct(
            ByteBuffer porBuf,
            CertificateFactory certFactory)
            throws SecurityException, IOException {
        int levelCount = 0;
        int lastSigAlgorithm = -1;
        X509Certificate lastCert = null;
        List<X509Certificate> certs = new ArrayList<>();
        List<Integer> flagsList = new ArrayList<>();
    
        // Proof-of-rotation struct:
        // A uint32 version code followed by basically a singly linked list of nodes, called levels
        // here, each of which have the following structure:
        // * length-prefix for the entire level
        //     - length-prefixed signed data (if previous level exists)
        //         * length-prefixed X509 Certificate
        //         * uint32 signature algorithm ID describing how this signed data was signed
        //     - uint32 flags describing how to treat the cert contained in this level
        //     - uint32 signature algorithm ID to use to verify the signature of the next level. The
        //         algorithm here must match the one in the signed data section of the next level.
        //     - length-prefixed signature over the signed data in this level.  The signature here
        //         is verified using the certificate from the previous level.
        // The linking is provided by the certificate of each level signing the one of the next.
    
        try {
    
            // get the version code, but don't do anything with it: creator knew about all our flags
            porBuf.getInt();
            HashSet<X509Certificate> certHistorySet = new HashSet<>();
            while (porBuf.hasRemaining()) {
                levelCount++;
                ByteBuffer level = getLengthPrefixedSlice(porBuf);
                ByteBuffer signedData = getLengthPrefixedSlice(level); // 获取当前level证书的信息
                int flags = level.getInt();
                int sigAlgorithm = level.getInt();
                byte[] signature = readLengthPrefixedByteArray(level); // 获取上一level证书为当前level证书生成的签名
    
                // 使用上一个level的证书去验证下一个level的证书
                if (lastCert != null) {
                    // 获取上一个证书的数据
                    Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams =
                            getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm);
                    // 获取上一个证书的公钥
                    PublicKey publicKey = lastCert.getPublicKey();
                    // 初始化签名信息
                    Signature sig = Signature.getInstance(sigAlgParams.first);
                    sig.initVerify(publicKey);
                    if (sigAlgParams.second != null) {
                        sig.setParameter(sigAlgParams.second);
                    }
                    // 设置当前level证书的数据
                    sig.update(signedData);
                    // 使用上一level证书为当前level证书生成的签名去验证当前level证书是否有效
                    if (!sig.verify(signature)) {
                        throw new SecurityException("Unable to verify signature of certificate #"
                                + levelCount + " using " + sigAlgParams.first + " when verifying"
                                + " Proof-of-rotation record");
                    }
                }
                            
                // 使用证书信息去创建证书,将其赋值给lastCert并将其丢入certs队列
                signedData.rewind();
                byte[] encodedCert = readLengthPrefixedByteArray(signedData);
                int signedSigAlgorithm = signedData.getInt();
                if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) {
                    throw new SecurityException("Signing algorithm ID mismatch for certificate #"
                            + levelCount + " when verifying Proof-of-rotation record");
                }
                lastCert = (X509Certificate)
                        certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
                lastCert = new VerbatimX509Certificate(lastCert, encodedCert);
    
                lastSigAlgorithm = sigAlgorithm;
                if (certHistorySet.contains(lastCert)) {
                    throw new SecurityException("Encountered duplicate entries in "
                            + "Proof-of-rotation record at certificate #" + levelCount + ".  All "
                            + "signing certificates should be unique");
                }
                certHistorySet.add(lastCert);
                certs.add(lastCert);
                flagsList.add(flags);
            }
        } catch (IOException | BufferUnderflowException e) {
            throw new IOException("Failed to parse Proof-of-rotation record", e);
        } catch (NoSuchAlgorithmException | InvalidKeyException
                | InvalidAlgorithmParameterException | SignatureException e) {
            throw new SecurityException(
                    "Failed to verify signature over signed data for certificate #"
                            + levelCount + " when verifying Proof-of-rotation record", e);
        } catch (CertificateException e) {
            throw new SecurityException("Failed to decode certificate #" + levelCount
                    + " when verifying Proof-of-rotation record", e);
        }
        return new VerifiedProofOfRotation(certs, flagsList);
    }
    
    

    V3版本校验流程

    实际上校验的时候并不需要从证书链中解析出最后的公钥,因为和V2的格式一样,直接可以在签名块中读取到公钥进行校验。所以他的流程前面的部分其实和v2版本是一致的,只不过在校验完成之后会再去验证证书链:

    1. 用PublicKey和Signature验证SignerData
    2. 用SignerData验证apk
    3. 验证当前安装的应用证书是否在证书链中
    4. 继续安装

    而证书链最新的证书公钥其实就是APK签名块里的PublicKey:

    private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs,
            List<X509Certificate> certs, CertificateFactory certFactory) throws IOException {
        X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
        VerifiedProofOfRotation por = null;
    
        while (attrs.hasRemaining()) {
            ByteBuffer attr = getLengthPrefixedSlice(attrs);
            if (attr.remaining() < 4) {
                throw new IOException("Remaining buffer too short to contain additional attribute "
                        + "ID. Remaining: " + attr.remaining());
            }
            int id = attr.getInt();
            switch(id) {
                case PROOF_OF_ROTATION_ATTR_ID:
                    if (por != null) {
                        throw new SecurityException("Encountered multiple Proof-of-rotation records"
                                + " when verifying APK Signature Scheme v3 signature");
                    }
                    por = verifyProofOfRotationStruct(attr, certFactory);
                    // 确认证书链最后一个证书的公钥与APK签名块的公钥相等
                    try {
                        if (por.certs.size() > 0
                                && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
                                        certChain[0].getEncoded())) {
                            throw new SecurityException("Terminal certificate in Proof-of-rotation"
                                    + " record does not match APK signing certificate");
                        }
                    } catch (CertificateEncodingException e) {
                        throw new SecurityException("Failed to encode certificate when comparing"
                                + " Proof-of-rotation record and signing certificate", e);
                    }
    
                    break;
                default:
                    // not the droid we're looking for, move along, move along.
                    break;
            }
        }
        return new VerifiedSigner(certChain, por);
    }
    

    最终用[最新的证书的公钥]+[摘要的签名]去验证[摘要]的有效性,从而验证apk的有效性:

    14.png

    这篇讲述了V2、V3签名机制的原理,由于章节已经很长了,渠道包的制作就放到下一篇。

    参考:

    VasDolly实现原理

    Android V2签名机制以及ApkSignerV2签名源码解析

    分析 Android V2 新签名打包机制

    Android P v3签名新特性

    一次让你搞懂Android应用签名

    相关文章

      网友评论

        本文标题:Android签名与渠道包制作-V2/V3签名原理

        本文链接:https://www.haomeiwen.com/subject/awqpkltx.html