Android簽名機制及PMS中校驗流程(雷驚風)

@Android簽名機制及PMS中校驗流程(雷驚風)

網上看到一篇比較好的關於Android簽名的文章,但是文章鏈接不安全,不知道哪天會不會找不到了,而且需要關注才能查看完整版,所以在這裏記錄一下,原文地址
.

一、簽名機制
衆所周知,在Android系統中,應用想要安裝到設備中,必須要有簽名才行,及時是debug的時候,開發工具也會對要運行的應用自動簽名,那麼我們先來了解一下這個簽名究竟是什麼。

首先Android系統爲了防止以安裝的應用被篡改,推出來的簽名自檢機制,來維護應用的安全性,可以說,簽名就是一個保護個人應用不受侵害的一種機制。而且這裏面說道了自檢,也就是說,在Android系統中,應用的簽名在安裝的過程中是自行檢查的,這點在本文的下面會詳細說道。
那麼,我們先來了解一下Android簽名的整個過程,這個過程大體可以分爲三個步驟,也對應了Android系統在校驗簽名時的三個步驟:

1、android應用打包apk的時候,會先對應用中所有的文件進行遍歷,做一次SHA1算法,也就是計算出文件的摘要信息,然後再用Base64進行編碼,然後將這些信息寫入到MANIFEST.MF文件中。這個文件在META-INF文件夾下面,解壓APK文件時就會看見了。當然,在對APK所有文件進行SHA1算法的時候,是不包括META-INF文件夾的(這句是廢話)。
下文是支付寶的MANIFEST.MF文件打開後的一部分(用記事本就可以打開);

Manifest-Version: 1.0
Created-By: 1.7.0_67 (Oracle Corporation)
Name: assets/emoji/emoji_195.png
SHA1-Digest: 2YVKFrBF45WOANd9G7zdpbAm6w0=
Name: assets/emoji/emoji_356.png
SHA1-Digest: VlPA09nbS5e3iae9WvAMq8O88BM=
Name: res/layout/envelope_notify.xml
SHA1-Digest: r4LUgyo6YoeqTvKQUyME82+XINs=
Name: lib/armeabi/libsgavmp.so
SHA1-Digest: DOwWbBL42qy+Z16EggSnCNYseDI=

從上文中可以看出,在MANIFEST.MF文件中,針對應用中每個文件的SHA1算法都會以鍵值對的方式寫入。這是爲了防止APK內容被篡改。想要驗證的話,可以通過安裝HashTab軟件(下載地址: http://www.baidu.com/s?wd=hashtab&rsv_spt=1&issp=1&f=8&rsv_bp=0&ie=utf-8&tn=baiduhome_pg&bs=hashtable ),然後找到對應的文件,右鍵用HashTab打開,就能看到對應的SHA1值,再到Base64網站上轉碼後對比一下就可以了(網址: http://tomeko.net/online_tools/hex_to_base64.php?lang=en )。

2、第二個步驟是將上文的MANIFEST.MF文件進行一次SHA1計算,然後同樣Base64轉碼後寫入到CERT.SF文件中,然後讀取MANIFEST.MF文件中子條目的值再次SHA1計算,Base64轉碼寫入到這個文件中。這個步驟同下面的第三個步驟統稱爲簽名,都是用的簽名工具來完成的。

Signature-Version: 1.0
Created-By: 1.0 (Android SignApk)
SHA1-Digest-Manifest: zg9ZRuDj9mgYGA7zbHu5lysUuxc=
Name: assets/emoji/emoji_195.png
SHA1-Digest: +HilKtU8N3AsfscyZ/3uHBc/5n4=
Name: assets/emoji/emoji_356.png
SHA1-Digest: 50goYIrW1+7aVHdrwsihe5t06pY=
Name: res/layout/envelope_notify.xml
SHA1-Digest: 1qtgFSOLlupySOaKR7m9pEgYrZM=

上文是支付寶應用中CERT.SF文件中的部分內容,我們可以發現確實和MANIFEST.MF文件中的內容類似(有的朋友可能會覺得這個步驟是重複的,下文中我會進行解釋)。

3、第三個步驟是關鍵的步驟了,也就是對APK進行加密的過程。我們在打包簽名的時候,開發工具會讓我們使用一個私鑰文件,如果沒有就會讓我們去創建一個。在Eclipse中,默認格式是.keystore文件,而在Android Studio中默認格式是.jks文件。這兩個文件都是私鑰庫文件,裏面存儲了一些信息,包括所有者、有效期、證書指紋及算法等。有了私鑰文件後,就可以對第二個步驟中生成的CERT.SF文件進行加密了,加密後會生成CERT.RSA文件。在Android中,總共有兩種簽名工具,一個是jarsigner,需要的是.keystore文件,而另一個是signapk簽名用的是pk8、x509.pem文件(keystore文件可以和pk8、x509.pem文件互相轉化)。這兩個簽名工具不同的是,jarsigner簽名後生成的.SF和.RSA的名字是私鑰庫中別名的名字,而signapk生成的兩個文件名則是默認的CERT。

生成.keystore文件是用的keytool.exe生成的,這個文件在jdk的bin目錄下。生成.keystore的指令如下(最好手打,有時候編碼問題,直接複製不一定能過):

keytool -genkey -v -keystore highball-key.keystore(文件名) -alias highball-key(別名) -keyalg RSA -keysize 2048 -validity 10000

查看私鑰文件的指令如下:

keytool -list -keystore a.keystore(keystore文件) -alias a(別名) -v

在這裏插入圖片描述
從上圖中可以看到私鑰庫文件中的內容。這個文件是簽名機制中最重要的文件,一定要保存好,如果丟失,那麼上架的應用想要更新可就麻煩了。
私鑰文件已經有了,就可以對APK進行加密了。加密的指令如下:

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore a.keystore(keystore文件) a.apk(apk文件) a-key(別名)

其實這一個指令可以完成整個這三個步驟,因爲要對應到Android系統的簽名校驗原理,所以分開來說明。
生成.RSA文件後用記事本打開是亂碼的,可以將後綴名改成.p7r。然後就可以打開查看了,截圖如下:
在這裏插入圖片描述
從上圖可以看出.RSA文件包含了加密後的公鑰和指紋,這兩個信息是非常重要的,在Android系統對apk簽名校驗的時候也要用到。手動加密完之後最好對apk進行對其操作:

zipalign -v 4 a.apk  a-release.apk(新的apk的命名)

校驗對齊是否成功:

zipalign -c -v 4 a-release.apk(apk名)

二、多重簽名
Android系統是支持多重簽名的,這是在簽名校驗時,系統只是對相同名的.SF文件和.RSA文件進行校驗,而且是在while循環中進行的校驗,也就是說可以不止一對.SF和.RSA文件,詳情在下文中會有說明。

如果要進行多重簽名,那麼經過本人多次試驗後得出的結果是,不能用Eclipse或者Android Studio簽名後的apk來進行二次簽名,而是要導出未簽名的apk。

導出未簽名的apk,在Android Studio的操作如下圖所示:

在Gradle視圖中,找到對應的項目下的assemble,執行雙擊操作,然後就會在module所在文件夾的outputs\apk文件夾中出現未簽名的apk。這個操作很簡單就不詳細描述了,得到的apk文件中是沒有META-INF文件夾的。

接下來就可以按照前文介紹的操作,生成兩個密鑰庫文件(.keystore文件)。然後對未簽名apk進行兩次簽名操作,得到了兩次簽名的apk文件,其META-INF文件夾內容如下:
在這裏插入圖片描述
從上圖我們可以發現,MANIFEST.MF文件只有一個,而.SF和.RSA文件則是有兩對,名字是對應密鑰庫文件的別名。OK多重簽名就成功搞定了,下面來看看在Android系統中,是如何針對應用簽名進行校驗的。

三、Android簽名校驗
Android系統下,針對應用的安裝是在PMS(Package Manager Service)中完成的, PMS是一個比較龐大的系統服務,我們今天只研究PMS中關於簽名校驗的部分。同apk簽名三步驟相對應,PMS中對簽名校驗也是分成了三個步驟,先是 驗證CERT.SF文件的摘要信息和CERT.RSA中的簽名信息數據是否一致、驗證MANIFEST.MF文件和CERT.SF文件是否匹配,最後是校驗 APK 中每個文件的算法同 MANIFEST.MF 文件中是否一致。
1、校驗CERT.SF文件和CERT.RSA文件
首先說明一下校驗這兩個文件的原理。Android系統對這兩個文件的校驗,是拿到.RSA文件的流對象,用ASN1解碼後拿到簽名數據,然後從簽名信息中分別獲取相關信息(發佈機構、序列號、簽名算法、公鑰等)。然後通過公鑰對簽名進行解密,獲得瞭解密後的數據同.SF文件進行對比。

Android 系統對應用的安裝過程是在 PackageManagerService 類中通過發送 handler 來處理針對包的一些消息的 , 比如說清理安裝包 , 發送安裝等 , 其中針對簽名校驗也在這裏處理。
在PackageManagerService.java中執行安裝的方法是installPackageLI(args,res);在這個方法中獲取了PackageParser對象,PackageParser.java是用來解析APK包的,也就是說簽名信息校驗的工作是在這個類中開始進行的。而執行校驗的方法是PackageParser.java中的collectCertificates方法:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        StrictJarFile jarFile = null;
        try {
            jarFile = new StrictJarFile(apkPath);

            // Always verify manifest, regardless of source
            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }
            ...
        }
}

這裏面new了一個StrictJarFile對象,這個類的構造方法中完成了CERT.SF文件和CERT.RSA文件中關於證書的校驗。因爲在這個構造方法中有個Boolean類型的變量的賦值,就是完成證書校驗的關鍵。那麼我們看一下這個變量是如何賦值的:

public StrictJarFile(String fileName) throws IOException {
        this.nativeHandle = nativeOpenJarFile(fileName);
        this.raf = new RandomAccessFile(fileName, "r");

        try {
            // Read the MANIFEST and signature files up front and try to
            // parse them. We never want to accept a JAR File with broken signatures
            // or manifests, so it's best to throw as early as possible.
            HashMap<String, byte[]> metaEntries = getMetaEntries();
            this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier = new JarVerifier(fileName, manifest, metaEntries);

            isSigned = verifier.readCertificates() && verifier.isSignedJar(); //完成校驗方法
        } catch (IOException ioe) {
            nativeClose(this.nativeHandle);
            throw ioe;
        }

        guard.open("close");
    }

從截圖中可以看到,總共執行了兩個方法,先看一下readCertificates方法:

synchronized boolean readCertificates() {
        if (metaEntries.isEmpty()) {
            return false;
        }

        Iterator<String> it = metaEntries.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
                verifyCertificate(key);
                it.remove();
            }
        }
        return true;
    }

從方法中可以看出,這個方法首先判斷一下是否有MANIFEST.MF文件,如果沒有就返回false,那麼isSigned就會被賦值爲false(這個變量的賦值會在後續方法中作爲校驗的參數)。然後會去找證書文件,而在android的項目中,一般多采用的是RSA加密算法,也就是生成的.RSA文件,所以這個方法會把.RSA文件找到,執行verifyCertificate方法:

private void verifyCertificate(String certFile) {
        // Found Digital Sig, .SF should already have been read
        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
        byte[] sfBytes = metaEntries.get(signatureFile);
        if (sfBytes == null) {
            return;
        }

        byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        // Manifest entry is required for any verifications.
        if (manifestBytes == null) {
            return;
        }

        byte[] sBlockBytes = metaEntries.get(certFile);
        try {
            Certificate[] signerCertChain = JarUtils.verifySignature(
                    new ByteArrayInputStream(sfBytes),
                    new ByteArrayInputStream(sBlockBytes)); // 校驗執行的位置
            if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
        } catch (IOException e) {
            return;
        } catch (GeneralSecurityException e) {
            throw failedVerification(jarName, signatureFile);
        }
    ...
}

這是方法的一部分,通過.RSA文件的名字找到對應的.SF文件,然後在獲取MANIFEST.MF文件和.RSA文件,到這裏三個文件就都拿到了。拿到三個文件數據後,會調用JarUtils.verifySignature方法, 這個方法,參數是.SF文件和.RSA文件的內存流對象;這個方法中通過用X509文件解析了.RSA文件,這個方法最終的目的是獲取到RSA文件中的證書;然後同.SF文件的數據進行對比,如果不相同會拋出異常:

...
if (md == null && daName != null) {
                md = MessageDigest.getInstance(daName);
            }
            if (md == null) {
                return null;
            }

            byte[] computedDigest = md.digest(sfBytes);
            if (!Arrays.equals(existingDigest, computedDigest)) {
                throw new SecurityException("Incorrect MD");
            }
...

然後回到JarVerifier.java的verifyCertificate方法中,會將之前拿到的簽名數據封裝到集合中:

...
if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
...

在創建StrictJarFile對象的時候,就完成了對.RSA和.SF文件的校驗操作。

2 、校驗MANIFEST.MF文件和.SF文件對應的摘要值
在JarVerifier.java中的verifyCertificate方法中,會繼續執行校驗MANIFEST.MF文件和.SF文件的操作:

private void verifyCertificate(String certFile) {
        // Found Digital Sig, .SF should already have been read
        ...
        // Verify manifest hash in .sf file
        Attributes attributes = new Attributes();
        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
        try {
            ManifestReader im = new ManifestReader(sfBytes, attributes);
            im.readEntries(entries, null);
        } catch (IOException e) {
            return;
        }

        // Do we actually have any signatures to look at?
        if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
            return;
        }

        boolean createdBySigntool = false;
        String createdBy = attributes.getValue("Created-By");
        if (createdBy != null) {
            createdBySigntool = createdBy.indexOf("signtool") != -1;
        }

        // Use .SF to verify the mainAttributes of the manifest
        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
        // file, such as those created before java 1.5, then we ignore
        // such verification.
        if (mainAttributesEnd > 0 && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes";
            if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
                throw failedVerification(jarName, signatureFile);
            }
        }

        // 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();
                Manifest.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);
                }
            }
        }
        metaEntries.put(signatureFile, null);
        signatures.put(signatureFile, entries);
    }

上面代碼中,前面一部分基本是校驗的準備工作,主要是封裝數據,真正的校驗操作是上文中的verify方法:

private boolean verify(Attributes attributes, String entry, byte[] data,
            int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            String algorithm = DIGEST_ALGORITHMS[i];
            String hash = attributes.getValue(algorithm + entry);
            if (hash == null) {
                continue;
            }

            MessageDigest md;
            try {
                md = MessageDigest.getInstance(algorithm);
            } catch (NoSuchAlgorithmException e) {
                continue;
            }
            if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
                md.update(data, start, end - 1 - start);
            } else {
                md.update(data, start, end - start);
            }
            byte[] b = md.digest();
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
        }
        return ignorable;
    }

調用這個方法就完成了MANIFEST.MF文件和.SF文件的對比.

3、校驗APK文件摘要值同MANIFEST.MF文件中對應值是否相同
接下來回到PackageParser.java中的collectCertificates方法,創建 StrictJarFile對象後, 會檢查一下是否有清單文件(AndroidManifest.xml),如果沒有則直接拋出異常。然後遍歷查詢到META-INF文件

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        ...

            // Always verify manifest, regardless of source
            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }

            final List<ZipEntry> toVerify = new ArrayList<>();
            toVerify.add(manifestEntry);

            // If we're parsing an untrusted package, verify all contents
            if ((flags & PARSE_IS_SYSTEM) == 0) {
                final Iterator<ZipEntry> i = jarFile.iterator();
                while (i.hasNext()) {
                    final ZipEntry entry = i.next();

                    if (entry.isDirectory()) continue;
                    if (entry.getName().startsWith("META-INF/")) continue;
                    if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;

                    toVerify.add(entry);
                }
            }

            // Verify that entries are signed consistently with the first entry
            // we encountered. Note that for splits, certificates may have
            // already been populated during an earlier parse of a base APK.
            for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                if (ArrayUtils.isEmpty(entryCerts)) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                            "Package " + apkPath + " has no certificates at entry "
                            + entry.getName());
                }
                final Signature[] entrySignatures = convertToSignatures(entryCerts);

                if (pkg.mCertificates == null) {
                    pkg.mCertificates = entryCerts;
                    pkg.mSignatures = entrySignatures;
                    pkg.mSigningKeys = new ArraySet<PublicKey>();
                    for (int i=0; i < entryCerts.length; i++) {
                        pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                        + " has mismatched certificates at entry "
                                        + entry.getName());
                    }
                }
            }
        } catch (GeneralSecurityException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                    "Failed to collect certificates from " + apkPath, e);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath, e);
        } finally {
            closeQuietly(jarFile);
        }
    }

對toVerify集合遍歷, 這裏面涉及到了最關鍵的方法:loadCertificates方法。

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
            throws PackageParserException {
        InputStream is = null;
        try {
            // We must read the stream for the JarEntry to retrieve
            // its certificates.
            is = jarFile.getInputStream(entry);
            readFullyIgnoringContents(is);
            return jarFile.getCertificateChains(entry);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
                    "Failed reading " + entry.getName() + " in " + jarFile, e);
        } finally {
            IoUtils.closeQuietly(is);
        }
    }

先看一下getInputStream方法:

public InputStream getInputStream(ZipEntry ze) {
        final InputStream is = getZipInputStream(ze);

        if (isSigned) {
            JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
            if (entry == null) {
                return is;
            }

            return new JarFile.JarFileInputStream(is, ze.getSize(), entry);
        }

        return is;
    }

在這個方法中,顯示根據isSigned進行判斷(上文中已經說明),然後獲取的VerifierEntry對象。在initEntry方法:

VerifierEntry initEntry(String name) {
        ...

        ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
        Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
            HashMap<String, Attributes> hm = entry.getValue();
            if (hm.get(name) != null) {
                // Found an entry for entry name in .SF file
                String signatureFile = entry.getKey();
                Certificate[] certChain = certificates.get(signatureFile);
                if (certChain != null) {
                    certChains.add(certChain);
                }
            }
        }

        // entry is not signed
        if (certChains.isEmpty()) {
            return null;
        }
        Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);

        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            final String algorithm = DIGEST_ALGORITHMS[i];
            final String hash = attributes.getValue(algorithm + "-Digest");
            if (hash == null) {
                continue;
            }
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);

            try {
                return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
                        certChainsArray, verifiedEntries);
            } catch (NoSuchAlgorithmException ignored) {
            }
        }
        return null;
    }

這個方法的目的是構造VerifierEntry對象,所以這個方法主要進行的就是準備構建這個對象的參數。這些參數分別是要驗證文件的名字。第二個是計算摘要的對象。第三個參數是對應文件的摘要值,從代碼中可以看到是ISO_8859_1格式的。第四個參數是證書鏈。最後一個參數是已經驗證過的文件列表。
VerifierEntry對象創建成功後,會返回JarFileInputStream流對象。然後回到PackageParser.java的loadCartificates方法中,會繼續走到readFullyIgnoringContents方法:

public static long readFullyIgnoringContents(InputStream in) throws IOException {
        byte[] buffer = sBuffer.getAndSet(null);
        if (buffer == null) {
            buffer = new byte[4096];
        }

        int n = 0;
        int count = 0;
        while ((n = in.read(buffer, 0, buffer.length)) != -1) {
            count += n;
        }

        sBuffer.set(buffer);
        return count;
    }

這裏面就是正常的讀取 read;但是我們從第一點中知道 , 這個 InputStream 對象實際上是 JarInputStream 對象 , 所以這裏面 read 執行的方法實際上是在 JarInputStream 中重寫的 read 方法。

@Override
        public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            if (done) {
                return -1;
            }
            if (count > 0) {
                int r = super.read(buffer, byteOffset, byteCount);
                if (r != -1) {
                    int size = r;
                    if (count < size) {
                        size = (int) count;
                    }
                    entry.write(buffer, byteOffset, size);
                    count -= size;
                } else {
                    count = 0;
                }
                if (count == 0) {
                    done = true;
                    entry.verify();  // 校驗MANIFEST.MF文件中數據和APK中每個文件的摘要值
                }
                return r;
            } else {
                done = true;
                entry.verify();
                return -1;
            }
        }

verify方法:

void verify() {
            byte[] d = digest.digest();
            if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
                throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
            }
            verifiedEntries.put(name, certChains);
        }

OK,通過這些方法的執行,便可以校驗了APK中各文件摘要值同MANIFEST.MF中對應的值是否相同 。

最後回到PackageParser.java中的collectCertificates方法,下面這個方法是上文中所有方法的初始位置:

public void collectCertificates(Package pkg, int flags) throws PackageParserException {
        pkg.mCertificates = null;
        pkg.mSignatures = null;
        pkg.mSigningKeys = null;

        collectCertificates(pkg, new File(pkg.baseCodePath), flags);

        if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
            for (String splitCodePath : pkg.splitCodePaths) {
                collectCertificates(pkg, new File(splitCodePath), flags);
            }
        }
    }

執行完了重載的collectCertificates方法後,會去判斷是否是一次安裝,如果不是第一次安裝,會再次執行 collectCertificates方法,如果簽名不一致,會拋出異常:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        ...
            for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                if (ArrayUtils.isEmpty(entryCerts)) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                            "Package " + apkPath + " has no certificates at entry "
                            + entry.getName());
                }
                final Signature[] entrySignatures = convertToSignatures(entryCerts);

                if (pkg.mCertificates == null) {
                    pkg.mCertificates = entryCerts;
                    pkg.mSignatures = entrySignatures;
                    pkg.mSigningKeys = new ArraySet<PublicKey>();
                    for (int i=0; i < entryCerts.length; i++) {
                        pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                        + " has mismatched certificates at entry "
                                        + entry.getName());
                    }
                }
            }
        } catch (GeneralSecurityException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                    "Failed to collect certificates from " + apkPath, e);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath, e);
        } finally {
            closeQuietly(jarFile);
        }
    }

總結一下,在APK簽名的時候,首先會遍歷APK所有的文件,生成對應的摘要數據寫入到META-INF文件夾的MENIFEST.MF文件中,接下來會將MENIFEST.MF文件的摘要值和它裏面每個數據塊的值重新做一次摘要算法,然後寫入到.SF文件中.最後是通過祕鑰庫文件對.SF文件進行加密,然後將公鑰數據,簽名數據等信息寫入到.RSA文件中.如上便是android下對APK的簽名機制.

而在android中對APK的簽名校驗則順序相反,先是解開.RSA文件,提取公鑰和簽名數據,找到對應的.SF文件進行對比.然後進行MENIFEST.MF和.SF文件的對比,最後則是對apk中各個文件的摘要數據同MANIFEST.MF文件進行校驗.不過在最後還會校驗在覆蓋安裝的時候,會再次執行校驗流程.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章