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

Android 7.0之后,新增了一个签名机制V2签名。

学习Android签名机制之前,需要你了解以下内容:

  • 数据摘要
  • 数字签名
  • 数字证书

简要的说明一下:

  • 数据摘要就是对一段数据进行散列算法计算得出的一段密文数据,过程不可逆,也就是不可解密。
  • 数字签名,就是用密钥对对数据加密或者签名,私钥加密的称为签名,使用公钥才能验证,公钥加密的就称为加密,使用私钥才能解密。
  • 数字证书,是为了保护签名者公钥的有效性,应当由权威的可信机构CA颁发。

以上最终目的只是保证内容的完整性以及防止被篡改。签名可以保证完整性,摘要可以保证是否篡改。

V1与V2区别

我们知道APK文件其实就是一个ZIP压缩文件,分为三部分,头文件、中央目录、结尾内容,那么V1和V2有什么区别:

  • V1签名只会检验第一部分的所有压缩文件,而不理会后两部分内容,缺少对APK的完整性校验,V2签名是针对整个APK进行校验(不包含签名块本身)。
  • V1中的数据摘要是基于原始未压缩文件计算的。因此在校验时,需要先解压出原始文件,这无疑是耗时的,而V2是对APK本身进行数据摘要计算,不存在解压APK的操作。

上述内容假设你对V1已经有了解。下面正式进入主题,分析V2签名源码,看一看V2签名过程。

V2签名机制概要

V2签名就是在ZIP的中央目录前并且紧挨着中央目录,增加了一块V2签名块存放签名信息。最终的签名APK,就是四块:头文件区、V2签名块、中央目录、尾部。

V2签名块

整个签名块的格式如下:

  • size of block,以字节数(不含此字段)计 (uint64)
  • 带 uint64 长度前缀的“ID-值”对序列:
  • size of block,以字节数计 - 与第一个字段相同 (uint64)
  • magic“APK 签名分块 42”(16 个字节)

在多个“ID-值”对中,APK签名信息的 ID 为 0x7109871a,包含的内容如下:
带长度前缀的 signer

  • 带长度前缀的 signed data,包含digests序列,X.509 certificates 序列,additional attributes序列
  • 带长度前缀的 signatures(带长度前缀)序列
  • 带长度前缀的 public key(SubjectPublicKeyInfo,ASN.1 DER 形式)

value可能会包含多个 signer,因为Android允许多个签名。
以上信息来自官网,我为了便于读者理解。简要的写了一下重点内容。
总结一下:一个签名块,可以包含多个ID-VALUE,APK的签名信息会存放在 ID 为 0x7109871a的键值对里。他的内容可以包含多个签名者的签名信息,每个签名信息下包含signed datasignaturespublic key,其中,signed data主要存放摘要序列、证书链、额外属性,signatures包含多个签名算法计算出来的签名值,public key表示签名者公钥,用于校验的时候验证签名的。

源码分析

简要的了解了一下V2签名机制,下面我们进入源码分析一下整个过程。
V2签名源码文件:ApkSignerV2.java
地址有墙,准备梯子。没有梯子也没关系,请看下面讲解:

(1)变量的定义
public abstract class ApkSignerV2 {
        //支持的签名算法列表
        public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
        public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
        public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
        public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
        public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
        public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
        public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
        public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302;
        public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
        // TODO: Adjust the value when signing scheme finalized.
        public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "1234567890";
        private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
        private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
        //计算摘要时的分片大小
        private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
        //魔数
        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,
                };
        //签名信息对应的 ID
        private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
        private ApkSignerV2() {}

上面列出了ApkSignerV2中所有定义的变量。可以看到APK_SIGNATURE_SCHEME_V2_BLOCK_ID 为固定值0x7109871a,以及一些支持的算法ID等。

(2)签名代码

签名方法为ApkSignerV2中sign函数,签名操作以及最终生成的APK都是由这个函数完成。函数比较长,主要分为 读APK、摘要、签名生成V2签名块,输出APK,我们主要看摘要、签名生成V2签名块这两部分,其余两部分都是对ZIP的操作,解析APK就是读取ZIP,输出APK就是写ZIP。

        public static ByteBuffer[] sign(
                ByteBuffer inputApk,
                List<SignerConfig> signerConfigs)
                throws ApkParseException, InvalidKeyException, SignatureException {
          
             //这里忽略读解析未签名的APK 代码。。。。

            //获取签名算法序列
            Set<Integer> contentDigestAlgorithms = new HashSet<>();
            for (SignerConfig signerConfig : signerConfigs) {
                for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
                    contentDigestAlgorithms.add(
                            getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
                }
            }
            Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
            try {
                //对APK内容进行摘要
                contentDigests =computeContentDigests(contentDigestAlgorithms,
                                new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
            } catch (DigestException e) {
                throw new SignatureException("Failed to compute digests of APK", e);
            }

            // 对APK的摘要进行数字签名,并生产V2签名块
            ByteBuffer apkSigningBlock = ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));

           //更新中央目录的偏移量,因为要添加一个V2块到原始的ZIP文件,中央目录偏移量也要改变
            centralDirOffset += apkSigningBlock.remaining();
            eocd.clear();
            ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
            originalInputApk.position(originalInputApk.limit());
            beforeCentralDir.clear();
            centralDir.clear();
            eocd.clear();
            // 将V2签名块,放置在中央目录之前
            return new ByteBuffer[] {
                    beforeCentralDir,
                    apkSigningBlock,
                    centralDir,
                    eocd,
            };
        }

上述代码。就是整个V2签名过程,我已经给注释好了。
在sign方法中,有一个参数List<SignerConfig> signerConfigs,这个是什么呢,我们看一下SignerConfig这个类。

/**
* Signer configuration.
 */
public static final class SignerConfig {
            /** Private key. */
            public PrivateKey privateKey;
            /**
             * Certificates, with the first certificate containing the public key corresponding to
             * {@link #privateKey}.
             */
            public List<X509Certificate> certificates;
            /**
             * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
             */
            public List<Integer> signatureAlgorithms;
        }

看官方注释,我们知道这个类表示签名者信息,包含私钥、证书序列、以及签名算法序列,而签名的时候传入的是一个List,说明可能会存在多个签名者signer进行签名,这也就对应着上面阐述到的签名块中包含多个signer签名信息。

(3)摘要计算

首先,说一下APK摘要计算规则,对于每个摘要算法,计算结果如下:

  • 将APK中文件内容块、中央目录、EOCD按照1MB大小分割成一些小块。
  • 计算每个小块的数据摘要,数据内容是0xa5 + 块字节长度 + 块内容。
  • 计算整体的数据摘要,数据内容是0x5a + 数据块的数量 + 每个数据块的摘要内容

总之,就是把APK按照1M大小分割,分别计算这些分段的摘要,最后把这些分段的摘要在进行计算得到最终的摘要。
在(2)中我们可知computeContentDigests()函数为计算APK摘要,我们进入方法内部,看其实现过程,主要跟着我的注释部分看:

        //参数digestAlgorithms是签名算法列表,contents则为文件内容块、中央目录、EOCD
        private static Map<Integer, byte[]> computeContentDigests(Set<Integer> digestAlgorithms,
                ByteBuffer[] contents) throws DigestException {

          // 按照1M大小 计算分段的数量
         //常量CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024*1024
            int chunkCount = 0;
            for (ByteBuffer input : contents) {
                chunkCount += getChunkCount(input.remaining(),CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
            }
          //开始计算分段摘要
          //digestsOfChunks记录分段摘要的数据,Integer对应签名算法,byte[]代表摘要
            final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
            for (int digestAlgorithm : digestAlgorithms) {
                //摘要后的结果大小
                int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
                //每个分片摘要结果长度的和
                byte[] concatenationOfChunkCountAndChunkDigests = new byte[5 + chunkCount * digestOutputSizeBytes];
                //每个分片摘要以0x5a连接
                concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
                setUnsignedInt32LittleEngian(chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
                digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
            }
            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);
                    //获取1个分片的内容
                    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);
                        }
                        // Reset position to 0 and limit to capacity. Position would've been modified
                        // by the preceding iteration of this loop. NOTE: Contrary to the method name,
                        // this does not modify the contents of the chunk.
                        chunk.clear();

                        setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);

                        md.update(chunkContentPrefix);
                        md.update(chunk);
                        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;
        }

我们来 看一下分段摘要代码:

int chunkIndex = 0;
            byte[] chunkContentPrefix = new byte[5];
            //分段以0xa5开始
            chunkContentPrefix[0] = (byte) 0xa5;

            // Optimization opportunity: digests of chunks can be computed in parallel.
            for (ByteBuffer input : contents) {
                while (input.hasRemaining()) {
                    int chunkSize = Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
                    //获取1个分片的内容
                    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);
                        }
                        chunk.clear();
                        setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);

                        //设置要被计算的数据
                        md.update(chunkContentPrefix);
                        md.update(chunk);
                        //用当前算法下的这个字节数组来接收计算的分段摘要
                        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++;
                }
            }

上面md.digest()方法就是最终计算一个分段摘要的方法,0xa5作为数据的开始,用concatenationOfChunkCountAndChunkDigests变量来接收的,而它的值是从digestsOfChunks根据签名算法ID获取来的一个byte[],所以digestsOfChunks集合是用来接收分段摘要后的数据的。for循环是做并行计算,直接把一个分段用所有的算法ID做了一遍摘要,分别存在digestsOfChunks对应的算法ID对应的Value中了。于是我们在看一下digestsOfChunks如何定义的:

          //计算分段的全部数量
            int chunkCount = 0;
            for (ByteBuffer input : contents) {
                chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
            }
            
            //digestsOfChunks记录分段摘要数据集合,Integer对应签名算法,byte[]代表摘要
            final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
            for (int digestAlgorithm : digestAlgorithms) {
                //当前算法下计算的摘要结果大小
                int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
                //所有分段摘要结果长度的和 +5
                byte[] concatenationOfChunkCountAndChunkDigests = new byte[5 + chunkCount * digestOutputSizeBytes];
                //最终的摘要以0x5a开始
                concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
                setUnsignedInt32LittleEngian(chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
                digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
            }

我们看到,循环所有签名算法ID,循环内部 计算了当前签名算法计算结果的长度,乘以分片的总数量,就是当前算法下,所有分段进行计算后合并在一起的长度,而且以0x5a开始,用于接收分段摘要数据,最终,put进digestsOfChunks集合。所以,digestsOfChunks里记录的都是每个算法下对应的分段摘要数据。那么根据摘要规则,最终的APK摘要数据,肯定是通过digestsOfChunks来计算,继续往下看computeContentDigests()方法中:

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;

如上,最终遍历digestsOfChunks来得到每个算法下的,最终摘要的集合。这个摘要集合就是要记录在V2签名块里的。这里,很多地方都在遍历List<Integer> signatureAlgorithms,这个就是收支持的签名算法集合,可以为一个或者多个,对应的也就是生成一个摘要或者多个摘要来保护APK的内容,同样的,也会对应生成一个或者多个签名。
到这里,整个摘要计算就结束了。返回的result就是APK的摘要序列了。下面我们看签名。

(4)签名计算

上面已经计算得到了APK的摘要,下面就是对摘要进行签名了,我们在进入sign()函数中,找到如下代码,这里就是签名,并生成V2签名块。

            // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
            ByteBuffer apkSigningBlock = ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));

我们重点看generateApkSigningBlock()函数。

 private static byte[] generateApkSigningBlock(List<SignerConfig> signerConfigs,
             Map<Integer, byte[]> contentDigests)throws InvalidKeyException, SignatureException {
   byte[] apkSignatureSchemeV2Block = generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
   return generateApkSigningBlock(apkSignatureSchemeV2Block);
 }

方法内部只有两行代码,一行就是计算签名内容,一行就是生成签名块。首先看generateApkSignatureSchemeV2Block()函数。

 private static byte[] generateApkSignatureSchemeV2Block(
                List<SignerConfig> signerConfigs,
                Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
            List<byte[]>= new ArrayList<>(signerConfigs.size());
            int signerNumber = 0;
            for (SignerConfig signerConfig : signerConfigs) {
                signerNumber++;
                byte[] signerBlock;
                try {
                    signerBlock = generateSignerBlock(signerConfig, contentDigests);
                } catch (InvalidKeyException e) {
                    throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
                } catch (SignatureException e) {
                    throw new SignatureException("Signer #" + signerNumber + " failed", e);
                }
                signerBlocks.add(signerBlock);
            }
            return encodeAsSequenceOfLengthPrefixedElements(
                    new byte[][] {
                            encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
                    });
        }

看代码,根据signerConfigs的个数,也就是签名者个数,来生成相应的signer信息块列表,最终返回一个V2信息块,其中generateSignerBlock()方法,主要生成V2信息块的,我们进入其内部看,代码较长,请跟着注释阅读:

private static byte[] generateSignerBlock(
                SignerConfig signerConfig,
                Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {

            //获取公钥
            PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
            byte[] encodedPublicKey = encodePublicKey(publicKey);

            //初始化signedData ,添加证书,添加摘要等
            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));
            }

            //将摘要序列digests添加到signedData
            signedData.digests = digests;

              //初始化singer,它就是一个签名信息数据
            V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();

            //将signer的signedData赋值
            signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
                    encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
                    // additional attributes
                    new byte[0],
            });
          ////将signer的publicKey 赋值
            signer.publicKey = encodedPublicKey;
            //下面代码,是对每个签名算法下的摘要进行签名,然后设置进signer.signatures
            signer.signatures = new ArrayList<>();
            for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {

                Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
                        getSignatureAlgorithmJcaSignatureAlgorithm(signature
                                Algorithm);
                String jcaSignatureAlgorithm = signatureParams.getFirst();

                AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
                byte[] signatureBytes;
                try {
                     //对signer中的 signedData进行签名
                    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 {
                   //用公钥对签名做一下验证,然后最终赋值给signer.signatures
                    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));
            }
            // 最终一个signer block的格式为:
            // * signed data,包含摘要/数字证书/额外属性等
            // *  signatures:
            // * public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
            return encodeAsSequenceOfLengthPrefixedElements(
                    new byte[][] {
                            signer.signedData,
                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
                                    signer.signatures),
                            signer.publicKey,
                    });
        }

综上,先实例化了一个signedData,然后实例化了一个signer,分别对其赋值,计算签名等等,最终返回去。

我们再回到generateApkSigningBlock函数:

 private static byte[] generateApkSigningBlock(List<SignerConfig> signerConfigs,
           Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
            byte[] apkSignatureSchemeV2Block = generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
            return generateApkSigningBlock(apkSignatureSchemeV2Block);
        }

第一步中的V2签名信息已经得到了,下面就是生成一个即将插入ZIP的V2签名块了,我们看generateApkSigningBlock方法:

private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
            // FORMAT:
            // uint64:  size (excluding this field)
            // repeated ID-value pairs:
            //     uint64:           size (excluding this field)
            //     uint32:           ID
            //     (size - 4) bytes: value
            // uint64:  size (same as the one above)
            // uint128: magic
            int resultSize =
                    8 // size
                            + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
                            + 8 // size
                            + 16 // magic
                    ;
            ByteBuffer result = ByteBuffer.allocate(resultSize);
            result.order(ByteOrder.LITTLE_ENDIAN);
            long blockSizeFieldValue = resultSize - 8;
            result.putLong(blockSizeFieldValue);
            long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
            result.putLong(pairSizeFieldValue);
            result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
            result.put(apkSignatureSchemeV2Block);
            result.putLong(blockSizeFieldValue);
            result.put(APK_SIGNING_BLOCK_MAGIC);
            return result.array();
        }

如上代码,最终生成一个V2签名块的格式返回。格式如下:
块长度、ID-Value、块长度、魔数。
其中签名信息的ID为0x7109871a,value值为apkSignatureSchemeV2Block里包含的数据。
到此,已经得到了一个签名块的内容,剩下的就是将这个签名块,插入到ZIP,回看sign函数:

// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
            ByteBuffer apkSigningBlock = ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
            // Update Central Directory Offset in End of Central Directory Record. Central Directory
            // follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
            centralDirOffset += apkSigningBlock.remaining();
            eocd.clear();
            ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
            // Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
            originalInputApk.position(originalInputApk.limit());
            // Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
            // Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
            // Contrary to the name, this does not clear the contents of these ByteBuffer.
            beforeCentralDir.clear();
            centralDir.clear();
            eocd.clear();
            // Insert APK Signing Block immediately before the ZIP Central Directory.
            return new ByteBuffer[] {
                    beforeCentralDir,
                    apkSigningBlock,
                    centralDir,
                    eocd,
            };

得到apkSigningBlock后,做了一些对中央目录偏移量的重新设置,最终返回一个签名的APK;

到此,整个APK签名源码中的整个流程也就分析完了,知道了最终生成的签名块格式是什么样的,在APK进行安装的时候,Android会对其进行校验,关于校验机制以及源码分析,我在下一篇在做讲解,到时候会连接进来!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容