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會對其進行校驗,關于校驗機制以及源碼分析,我在下一篇在做講解,到時候會連接進來!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容