1. 程式人生 > >安卓防簽名策略

安卓防簽名策略

標籤(空格分隔): 安卓簽名


安卓打包過程可參考google給出的APK打包流程圖:

最終通過apkbuilder生成的apk實際上最終的儲存就是一個zip壓縮包,因此可以參考zip壓縮包的儲存格式來理解apk的儲存。

一、jarsigner v1簽名方式

1、jarsigner簽名方案

jarSigner簽名方式看命名實際上是Jar的一種簽名方式,由JDK提供,jarSigner簽名後會生成一個META-INF資料夾,META-INF資料夾包含如下內容:

1、MANIFEST.MF檔案:這個檔案包含了APK壓縮包的所有檔案對應的摘要資訊,每個檔案路徑和對應的摘要資訊都列舉出來:

Name: lib/armeabi/libCollect.so
SHA-256-Digest: VAlz6QwJBoJ1mFMJTuzeA9sZ6m8e1QNGvE/KJ6iSa2c=

Name: res/drawable/upgradeVersion.xml
SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho=
複製程式碼

2、CERT.SF檔案:CERT.SF是MF檔案的摘要資訊,以及每一條檔案摘要的base64編碼;

3、CERT.RSA:對CERT.SF檔案用私鑰加密,得到的密文儲存到CERT.RSA檔案,CERT.RSA檔案還會存放證書資訊、公鑰資訊。

以上過程實質就是一個數字簽名的過程,這個數字簽名是要保證壓縮包的檔案不會被篡改,壓縮包的檔案摘要經過處理後被私鑰加密;安卓驗證APK安裝就是這樣的處理。

2、jarsigner簽名的APK安裝驗證過程

對於jarsigner簽名的APK,安裝時候驗證簽名可以參考 SDK原始碼:sdk/sources/$sdkversion/android/util/jar,驗證主要包括兩個部分:第一步實際就是數字簽名驗證過程,通過CERT.RSA檔案驗證CERT.SF檔案,確保證CERT.SF檔案正確,原始碼如下:

StrictJarVerifier.java
synchronized boolean readCertificates
(){ ...... while (it.hasNext()) { String key = it.next(); if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) { verifyCertificate(key); it.remove(); } } } static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)throws GeneralSecurityException { ...... } 複製程式碼

jarsign原則上也支援多種簽名; 第二步是通過.SF檔案驗證.MF檔案對應的摘要資訊,確.MF檔案檔案未被篡改:

private void verifyCertificate(String certFile) {
    ......
    byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        // Manifest entry is required for any verifications.
        if (manifestBytes == null) {
            return;
        }
        
    ......
    // Use .SF to verify the whole manifest.
        String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
        if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, Attributes> entry = it.next();
                StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", manifestBytes,
                        chunk.start, chunk.end, createdBySigntool, false)) {
                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
                }
            }
        }
}

複製程式碼

最後通過.MF檔案確認每一個檔案對應的摘要資訊,以此來確保APK解壓後的檔案都沒有被修改過;

3、jarsigner簽名的缺陷

根據上面安卓校驗jarsigner簽名的過程,可以看到jarsigner簽名後的APK有如下缺陷: 1、重簽名成本很低,APK解壓後刪除META-INF資料夾,重新簽名即可; 2、每次安裝APK要先解壓APK再做校驗,解壓APK既耗時又耗費系統資源,安裝過程體驗不好; 雖然針對容易被重簽名的問題,可以有一些解決辦法:

1.可以利用壓縮包的File Comment檔案註釋區域寫入一些安全資訊,在APK啟動的時候通過讀取安全資訊來防篡改;具體思路也是利用壓縮包的File Comment區域不影響壓縮包檔案本身;

2.剛剛的簽名驗證過程只會檢驗META-INF資料夾三個檔案,可以在META-INF存放其他檔案寫入安全資訊,此安全不會被安卓安裝過程校驗,APK啟動後做校驗即可;

但是從安全性來說,jarsigner簽名被破解的成本太低,因此Google提出了APK Signature Scheme v2簽名。

三、APK Signature Scheme v2簽名

1、apksigner簽名

Android 7.0引入了新的應用簽名方案APK Signature Schemev2,APK簽名方案v2是基於APK二進位制檔案的,即簽名和安裝校驗都是基於APK二進位制檔案的,即只要二進位制檔案發生改變,就認為APK被修改了。 apksigner簽名前後APK檔案內容如下:

v2簽名後在Central Directory塊前生成一個APK Signing Block,儲存的就是v2簽名和簽名者身份資訊; apk簽名塊的結構如下

偏移 位元組數 描述
@+0 8 這個Block的長度(本欄位的長度不計算在內)
@+8 n 一組ID-value
@-24 8 這個Block的長度(和第一個欄位一樣值)
@-16 16 魔數"APK Sign Block 42"

APK的v2簽名的ID-value可以儲存多個Id-值對,其中會被校驗的"ID-值"對的ID為0x7109871a,其他ID未知的"ID-值"對不會被解譯;此處可以做為一個漏洞,美團新的渠道包方案就是利用了這個漏洞; 通過分析安卓對於v2簽名檔案的原始碼可知,在簽名前,安卓生成的 APK是一個壓縮二進位制檔案,v2簽名後也會生成一個對應的SF檔案,SF檔案裡面有個標誌 X-Android-APK-Signed ,

判斷是否有v2簽名這個標誌,對應命令: apksigner verify 執行這個命令的原始碼其實就是:

java原始碼

/**
 * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
 * associated with each signer.
 *
 * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
 * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
 *         verify.
 * @throws IOException if an I/O error occurs while reading the APK file.
 */
private static X509Certificate[][] verify(RandomAccessFile apk)
        throws SignatureNotFoundException, SecurityException, IOException {
    SignatureInfo signatureInfo = findSignature(apk);
    return verify(apk.getFD(), signatureInfo);
}

複製程式碼

首先我們需要找到對應的APK Signing Block ,話不多說,直接看原始碼:

private static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
    ......
    // Find the APK Signing Block. The block immediately precedes the Central Directory.
    long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
    Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
    ......
複製程式碼

由上面原始碼邏輯可以看到,首先需要找到Central Directory,然後根據儲存結構找到前面的Signing Block,怎麼去確定是否有生成Signing Block呢?看程式碼是如何實現的?

private static Pair<ByteBuffer, Long> findApkSigningBlock(
    RandomAccessFile apk, long centralDirOffset)
    throws IOException, SignatureNotFoundException {
    ......
    
    // Read the magic and offset in file from the footer section of the block:
    // * uint64:   size of block
    // * 16 bytes: magic
    ByteBuffer footer = ByteBuffer.allocate(24);
    footer.order(ByteOrder.LITTLE_ENDIAN);
    apk.seek(centralDirOffset - footer.capacity());
    apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
    if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
            || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
        throw new SignatureNotFoundException(
                "No APK Signing Block before ZIP Central Directory");
    }
    ......
複製程式碼

需要關注如下兩個值:

private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
複製程式碼

如下真正去生成Apk Signing Block的程式碼需要結合Apk的二進位制小端序結構去分析,具體程式碼如下:

private static Pair<ByteBuffer, Long> findApkSigningBlock(
    RandomAccessFile apk, long centralDirOffset)
    throws IOException, SignatureNotFoundException {
    ......
    
    int totalSize = (int) (apkSigBlockSizeInFooter + 8);
    long apkSigBlockOffset = centralDirOffset - totalSize;
    if (apkSigBlockOffset < 0) {
        throw new SignatureNotFoundException(
                "APK Signing Block offset out of range: " + apkSigBlockOffset);
    }
    ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
    apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
    apk.seek(apkSigBlockOffset);
    apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
    long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
    ......
複製程式碼

APK Signing Block APK簽名分塊裡面儲存有 APK簽名方案V2分塊,關於其查詢過程,可以參考原始碼

java原始碼

private static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
    ......
    // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
    ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
    ......
複製程式碼

APK Signing Block 內部多位元組物件儲存方式採用的是LITTLE_ENDIAN小端序; 一定要記得APK Signing Block內部的儲存內容,由於採用小端序,前面32個位元組的資料是固定的,用來儲存長度和Scheme v2分塊,由於用來儲存ID-Value的區域是不固定的,因此整個簽名分塊的長度是未知的,因此就有對應的標誌長度的欄位; 對應原始碼:

private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
    throws SignatureNotFoundException {
    checkByteOrderLittleEndian(apkSigningBlock);
    // FORMAT:
    // OFFSET       DATA TYPE  DESCRIPTION
    // * @+0  bytes uint64:    size in bytes (excluding this field)
    // * @+8  bytes pairs
    // * @-24 bytes uint64:    size in bytes (same as the one above)
    // * @-16 bytes uint128:   magic
    ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
    ......
    
    if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
        return getByteBuffer(pairs, len - 4);
    }
    ......

    throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
}
複製程式碼

仔細觀察上面的程式碼,給的ByteBuffer的起始位置資訊,其實這是因為目前的儲存結構是小端序,因此實際給出的ByteBuffer就是去掉ID-Value剩下的值;小黑板:在v2簽名驗證過程中,用來儲存Id-Value的區間被過濾掉不做檢查,安卓在v2簽名也留下來給大家可以利用的區間;

為什麼會有兩個區域用來儲存簽名分塊的長度呢? 尋找APK簽名方案v2分塊的過程是以ID:0x7109871a為標誌,找到對應的value值,這個ID標誌位很重要,其他所有的value值都是根據這個ID索引得到的; 而這個APK Signature Scheme v2 Block儲存的資料signer由幾部分組成,第一個是signed data儲存將APK內容按照一定規則分塊計算摘要,採用兩級樹方式,最終得到的摘要資訊;第二個是signatures儲存當前簽名所採用的簽名演算法,目前可以支援的計算摘要演算法有7種,而對應的摘要演算法又有對應的加密演算法,因此這個欄位儲存了簽名演算法;第三個是帶長度字首的public key(SubjectPublicKeyInfo,ASN.1 DER 形式),即剛剛用來加密的私鑰對應的公鑰資訊;以上就是APK Signature Scheme v2 Block的資料儲存結構,可以直觀的看下圖:

APK資料是很大,如果直接採用非對稱加密資料,效果是非常慢的,那如何做簽名呢? —— 答案是對APK受保護的資料直接按照一定規則分塊,然後對分塊分塊計算摘要,再採用兩級樹方式,將剛剛得到的分塊摘要再按照一定規則計算得到最終摘要;非對稱加密直接私鑰加密最終摘要資訊; 如上的簽名方案是否還有加快計算速度的方案?—— 可以先提前分塊,然後考慮並行處理計算分塊摘要,大大提高計算速度;

上面的過程對應原始碼實現:

private static X509Certificate[] verifySigner(
            ByteBuffer signerBlock,
            Map<Integer, byte[]> contentDigests,
            CertificateFactory certFactory) throws SecurityException, IOException {
    ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
    ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
    byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
    ......
    //get signature,consider litte-edian
    while (signatures.hasRemaining()) {
        signatureCount++;
        try {
            ByteBuffer signature = getLengthPrefixedSlice(signatures);
            ......
            int sigAlgorithm = signature.getInt();
            signaturesSigAlgorithms.add(sigAlgorithm);
            if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
                continue;
            }
            if ((bestSigAlgorithm == -1)
                    || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
                bestSigAlgorithm = sigAlgorithm;
                bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
            }
        } catch (IOException | BufferUnderflowException e) {
            ......
        }
    }
    
    ......
    //verify signed data
    try {
        PublicKey publicKey =
                KeyFactory.getInstance(keyAlgorithm)
                        .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
        Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
        sig.initVerify(publicKey);
        if (jcaSignatureAlgorithmParams != null) {
            sig.setParameter(jcaSignatureAlgorithmParams);
        }
        sig.update(signedData);
        sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
    } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
            | InvalidAlgorithmParameterException | SignatureException e) {
        throw new SecurityException(
                "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
    }
    if (!sigVerified) {
        throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
    }
    ......
    
    //get digest of signed data
    while (digests.hasRemaining()) {
        digestCount++;
        try {
            ByteBuffer digest = getLengthPrefixedSlice(digests);
            if (digest.remaining() < 8) {
                throw new IOException("Record too short");
            }
            int sigAlgorithm = digest.getInt();
            digestsSigAlgorithms.add(sigAlgorithm);
            if (sigAlgorithm == bestSigAlgorithm) {
                contentDigest = readLengthPrefixedByteArray(digest);
            }
        } catch (IOException | BufferUnderflowException e) {
            throw new IOException("Failed to parse digest record #" + digestCount, e);
        }
    }
    ......
    //verify digest 
    int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
    byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
    if ((previousSignerDigest != null)
            && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
        throw new SecurityException(
                getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
                + " contents digest does not match the digest specified by a preceding signer");
    }
    ......
    
    //get public key
    ByteBuffer certificates = getLengthPrefixedSlice(signedData);
    List<X509Certificate> certs = new ArrayList<>();
    int certificateCount = 0;
    while (certificates.hasRemaining()) {
        certificateCount++;
        byte[] encodedCert = readLengthPrefixedByteArray(certificates);
        X509Certificate certificate;
        try {
            certificate = (X509Certificate)
                    certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
        } catch (CertificateException e) {
            throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
        }
        certificate = new VerbatimX509Certificate(certificate, encodedCert);
        certs.add(certificate);
    }

    //verify public key
    if (certs.isEmpty()) {
        throw new SecurityException("No certificates listed");
    }
    X509Certificate mainCertificate = certs.get(0);
    byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
    if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
        throw new SecurityException(
                "Public key mismatch between certificate and signature record");
    }

    return certs.toArray(new X509Certificate[certs.size()]);

複製程式碼

上面的原始碼比較多,但是大致的邏輯一樣,都是先得到對應的signature、digest of signed data、public Key,然後分別都要做verify;

上面做了分別verify之後,接下來要做完整性校驗,也就是驗證我們的簽名邏輯,直接看原始碼是如何處理的?

private static void verifyIntegrity(
    Map<Integer, byte[]> expectedDigests,
    FileDescriptor apkFileDescriptor,
    long apkSigningBlockOffset,
    long centralDirOffset,
    long eocdOffset,
    ByteBuffer eocdBuf) throws SecurityException {
                // We need to verify the integrity of the following three sections of the file:
    // 1. Everything up to the start of the APK Signing Block.
    // 2. ZIP Central Directory.
    // 3. ZIP End of Central Directory (EoCD).
    // Each of these sections is represented as a separate DataSource instance below.

    // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
    // avoid wasting physical memory. In most APK verification scenarios, the contents of the
    // APK are already there in the OS's page cache and thus mmap does not use additional
    // physical memory.
    DataSource beforeApkSigningBlock =
            new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
    DataSource centralDir =
            new MemoryMappedFileDataSource(
                    apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);

    // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
    // Central Directory must be considered to point to the offset of the APK Signing Block.
    eocdBuf = eocdBuf.duplicate();
    eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
    ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
    ......
    //計算最終摘要
    try {
        actualDigests =
                computeContentDigests(
                        digestAlgorithms,
                        new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
    } catch (DigestException e) {
        throw new SecurityException("Failed to compute digest(s) of contents", e);
    }
    ......
        
    }
複製程式碼

computeContentDigests就是整個計算摘要的函式,具體原始碼可以自行閱讀,上面已經簡要說明其原理;如上就是APK安裝時候,安卓系統驗證是否使用v2簽名的過程;

從上面v2簽名的過程來看,其相對於jarsigner方式,想要做二次簽名就是需要熟悉v1簽名過程,考慮針對二進位制檔案去掉對應的APK Signing Block再重新簽名,實際上也是可以實現的; APKSigner想對於JarSigner的優先兩個:1、簽名更快,安裝時候驗證簽名也更快,直接對二進位制檔案操作,而不需要像jarsigner那樣需要先壓縮檔案簽名,先解壓檔案驗證簽名,效率太低;2、安全性更好,使用者想要抹掉簽名重新修改檔案的成本更高,需要對整個ApkSigner原理非常清楚,二次簽名的成本更高; 但是如上面所述,ApkSigner並不能防止二次簽名,要防二次簽名需要有其他方案;

v2分塊的本質就是數字簽名的過程,因此會儲存對應的加解密資訊和摘要資訊; 每一個APK簽名方案v2分塊對應一個簽名者/身份簽名,有多個簽名者則含有多個v2分塊;v2分塊結構資訊如下: v2分塊是用來保護APK全檔案的,明文即APK全檔案資訊,v2分塊儲存了摘要演算法和摘要資訊,同時儲存了數字證書資訊和加密演算法資訊,並提供公鑰資訊;類似數字簽名的校驗一致,最終通過計算就能夠證明APK已經做了v2簽名,且apk內容沒有被篡改; 目前APK Signature演算法支援的主流的摘要演算法,同時支援RSA、DSA、EC橢圓加密等非對稱演算法; APK Signature演算法實質是通過對APK全內容做類似數字簽名的工作,來保證APK檔案不會被篡改。

2、APK Signature保護APK內容的實現

首先打包的APK轉換成zip其檔案結構如下: ?問題1:可以把APK當作Zip檔案來處理,但是Zip結構是有幾個限制條件的,比如zipcommentfield對應到什麼內容,zip eocd會有 comment field?

APK Signature演算法主要做了兩個事情: a,計算第1、3、4部分內容的摘要,將這些摘要資訊儲存到APK Signing Block的v2分塊的signed data分塊;(?是對最終的頂級摘要加密簽名還是對每個分塊摘要都加密簽名————最終計算得到的頂級摘要資訊儲存到singed data分塊,因為最終只是按照規則計算最終摘要相同即可;) b,將上面得到的分塊(摘要資訊)通過一個或多個加密演算法來加密;(?對頂級摘要可能採用一個或多個簽名來保護————為了安全性考慮,可能採用多種加密簽名方式來保護,這個只是為了增加Signing Block資料的安全性而已;) 從上面的步驟可以看到,這實際上就是數字簽名的實現方式; 計算分塊的策略如下,先將資訊分成1MB的連續塊,然後分別計算每塊的摘要,可以通過並行處理加快計算速度;然後將得到的分塊摘要再按照規則計算得到最終的頂級摘要;

3、APK安裝驗證流程:

Google官方驗證APK流程圖:

驗證v2簽名的流程:
複製程式碼

1、先找到APK Signing Block,程式碼如下:

ApkSignatureSchemeV2Verifier.java
public static X509Certificate[][] verify(String apkFile)
            throws SignatureNotFoundException, SecurityException, IOException {
        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
            return verify(apk);
        }
    }
    
private static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
    ......
    long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
        Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
        long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
    .....
}
複製程式碼

2、對於v2分塊的每個signer做驗證,首先找到此signer所採用的加密演算法,然後對signed data做解密,確保得到了正確的摘要資訊,程式碼如下:

private static X509Certificate[][] verify(
            FileDescriptor apkFileDescriptor,
            SignatureInfo signatureInfo) throws SecurityException {
    ......
    while (signers.hasRemaining()) {
            signerCount++;
            try {
                ByteBuffer signer = getLengthPrefixedSlice(signers);
                X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
                signerCerts.add(certs);
            } catch (IOException | BufferUnderflowException | SecurityException e) {
                throw new SecurityException(
                        "Failed to parse/verify signer #" + signerCount + " block",
                        e);
            }
        }
    ......
}
複製程式碼

3、然後驗證最終的摘要資訊是否正確,只要頂級摘要是正確的,表明摘要資訊就是沒有被篡改的,程式碼如下:

private static void verifyIntegrity(
            Map<Integer, byte[]> expectedDigests,
            FileDescriptor apkFileDescriptor,
            long apkSigningBlockOffset,
            long centralDirOffset,
            long eocdOffset,
            ByteBuffer eocdBuf) throws SecurityException {
    ......
            try {
            actualDigests =
                    computeContentDigests(
                            digestAlgorithms,
                            new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
        } catch (DigestException e) {
            throw new SecurityException("Failed to compute digest(s) of contents", e);
        }
        for (int i = 0; i < digestAlgorithms.length; i++) {
            int digestAlgorithm = digestAlgorithms[i];
            byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
            byte[] actualDigest = actualDigests[i];
            if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
                throw new SecurityException(
                        getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
                                + " digest of contents did not verify");
            }
        }
}

private static byte[][] computeContentDigests(
            int[] digestAlgorithms,
            DataSource[] contents) throws DigestException {
    ......
    for (DataSource input : contents) {
            long inputOffset = 0;
            long inputRemaining = input.size();
            while (inputRemaining > 0) {
                int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
                for (int i = 0; i < mds.length; i++) {
                    mds[i].update(chunkContentPrefix);
                }
        ......
    }
    
    for (int i = 0; i < digestAlgorithms.length; i++) {
            int digestAlgorithm = digestAlgorithms[i];
            byte[] input = digestsOfChunks[i];
            String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
            MessageDigest md;
            try {
                md = MessageDigest.getInstance(jcaAlgorithmName);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
            }
            byte[] output = md.digest(input);
            result[i] = output;
    }
    ......    
}

複製程式碼