Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android App簽名(證書)校驗過程源碼分析

Android App簽名(證書)校驗過程源碼分析

編輯:關於Android編程

Android App安裝是需要證書支持的,我們在Eclipse或者Android Studio中開發App時,並沒有注意關於證書的事,也能正確安裝App。這是因為使用了默認的debug證書。在Android App升級的時候,證書發揮的作用就尤為明顯了。只有證書相同時,才能對App進行升級。證書也是為了防止App偽造的,屬於Android安全策略的一部分。另外,Android沙箱機制中,也和證書有關。兩個App如要共享文件,代碼,或者資源時,需要使用shareUid屬性,只有證書相同的App的才能shareUid。才外,如果一個App中申明了signature級別的權限,也是只有和那個App簽名相同的App才能申請到對應的權限。

??雖然之前也了解過Android App的簽名校驗過程,但都是根據別人總結的結果,沒有自己動手分析Android源碼。所以本篇Blog將從源碼出發分析Android App的簽名校驗過程,分析完源碼之後,也會和網上大多數的資料一樣給出總結。

一、 源碼分析

??上篇BlogPackageInstaller源碼分析中,程序安裝過程調用了installPackageLI()方法。而在installPackageLI()方法內部,調用了collectCertificates()方法,從而進入了App的簽名檢驗過程。下面我們查看collectCertificates()的源碼實現,源碼路徑:/frameworks/base/core/java/android/content/pm/PackageParser.java

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);
        }
    }
}
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");
        }

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

        // If we're parsing an untrusted package, verify all contents
        if ((flags & PARSE_IS_SYSTEM) == 0) {
            final Iterator 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();
                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);
    }
}

??在collectCertificates(Package pkg, File apkFile, int flags)函數裡面,首先提取apk的manifest.xml文件。

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 toVerify = new ArrayList<>();
toVerify.add(manifestEntry);

??然後,程序遍歷apk文件的所有文件節點,把除了META-INF/文件夾裡面的文外外的所以文件加入待檢驗List。

// If we're parsing an untrusted package, verify all contents
if ((flags & PARSE_IS_SYSTEM) == 0) {
    final Iterator 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);
    }
}

??緊接著把所以節點傳入loadCertificates()方法,

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();
       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());
       }
   }
}

??要知道loadCertificates()的作用需要分析其方法實現原型。在PackageParser.java中實現了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);
   }
}

??在StrictJarFile.java中,實現了getCertificateChains()方法,代碼路徑/libcore/luni/src/main/java/java/util/jar/StrictJarFile.java。

public Certificate[][] getCertificateChains(ZipEntry ze) {
if (isSigned) {
   return verifier.getCertificateChains(ze.getName());
}

return null;
}

??StrictJarFile.java中的getCertificateChains()繼續調用JarVerifier中的getCertificateChains()方法,代碼路徑:/libcore/luni/src/main/java/java/util/jar/JarVerifier.java。

Certificate[][] getCertificateChains(String name) {
    return verifiedEntries.get(name);
}
private final Hashtable verifiedEntries=new Hashtable();

??verifiedEntries僅僅是JarVerifier中的一個變量,所以重點要查看verifiedEntries是怎樣被賦值的。我們暫時把這個問題先放到後面處理。

??在PackageParser.java中的collectCertificates(Package pkg, File apkFile, int flags)函數中,調用final Certificate[][] entryCerts = loadCertificates(jarFile, entry)前,先對jarFile進行了實例化,我們根據StrictJarFile的構造函數查看一下實例化過程。代碼路徑:/libcore/luni/src/main/java/java/util/jar/StrictJarFile.java。

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 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");
}

private HashMap getMetaEntries() throws IOException {
    HashMap metaEntries = new HashMap();

    Iterator entryIterator = new EntryIterator(nativeHandle, "META-INF/");
    while (entryIterator.hasNext()) {
        final ZipEntry entry = entryIterator.next();
        metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
}

return metaEntries;
}

??JarVerifier構造函數。

JarVerifier(String name, Manifest manifest, HashMap metaEntries) {
    jarName = name;
    this.manifest = manifest;
    this.metaEntries = metaEntries;
    this.mainAttributesEnd = manifest.getMainAttributesEnd();
}

??從上面的源碼可以看出,getMetaEntries()就是從apk的META-INF/文件夾中讀取文件,並把結果存儲起來,存儲形式是文件名為鍵文件byte內容為值得鍵值對。

??回到StrictJarFile.java文件中的構造函數,裡面還有一行代碼與JarVerifier有關,即isSigned = verifier.readCertificates() && verifier.isSignedJar()。isSignedJar()函數比較簡單,就是根據JarVerifier的certificates變量是否為空來判定Jar是否被簽過名。在JarVerifier中查看readCertificates()源碼。

boolean isSignedJar() {
    return certificates.size() > 0;
}
synchronized boolean readCertificates() {
if (metaEntries.isEmpty()) {
    return false;
}

Iterator 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;
}

??這個函數從META-INF/文件夾中提取以.DSA或.RSA或.EC結尾的文件,然後交給verifyCertificate(key)函數處理。所以我們查看verifyCertificate(key)函數實現。

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);
}

// Verify manifest hash in .sf file
Attributes attributes = new Attributes();
HashMap entries = new HashMap();
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> it = entries.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry 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);
}

??這個方法中,首先提取[cert].SF文件,MANIFET.MF文件。然後把[cert].SF文件和參數傳遞進來的[cert].RSA(或.DSA或.EC)文件交給JarUtils.verifySignature()方法處理,verifySignature()所在源碼路徑/libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java。但是這裡我先不討論這個函數,後面留下一個關於簽名檢驗過程的疑問,可能會在對這個疑問的解決中重新查看這個函數源碼,有可能是一個很長的話題。

private void verifyCertificate(String 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);
    }

    ````    
}

??所以根據資料的說法,verifySignature()函數功能是驗證[CERT].RSA文件中包含的對[CERT].SF的簽名是否正確。如果驗證失敗,則拋出GeneralSecurityException異常,進而調用failedVerification()函數拋出SecurityException異常。如果校驗成功,則返回簽名的證書鏈。至於證書鏈Certificate[]的數據結構,也在後面繼續分析verifySignature()時討論。

private static SecurityException failedVerification(String jarName, String signatureFile) {
    throw new SecurityException(jarName + " failed verification of " + signatureFile);
}

??我們繼續verifyCertificate()函數的分析,下面就是對MANIFEST.MF文件中的各個條目的簽名值與[CERT].SF文件中保存的條目進行對比。

private void verifyCertificate(String certFile) {

    ````

     // 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);
         }
     }

    ````

}

 

MANIFEST.MF

 <喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxjZW50ZXI+DQoJPGltZyBhbHQ9".SF" src="/uploadfile/Collfiles/20160926/20160926101104405.jpg" title="\" />
??這裡首先判斷是否由工具簽名,判斷方法是根據[CERT].SF文件中的Created-By條目中是否由signtool關鍵字,若有,說明是工具簽名,則檢驗MANIFEST.MF文件的頭部的hash與[CERT].SF中記錄的條目SHA1-Digest-Manifest-Main-Attributes: KdSJo1gAKJkR4HRZDprFCj1n3S4=是否匹配。接著,就是檢驗MANIFEST.MF中的所有條目的hash值與[CERT].SF中所記錄的對應條目是否匹配。若不匹配,說明MANIFET.MF文件遭到修改。

 

        // Use .SF to verify the whole manifest.
String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
    Iterator> it = entries.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry 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);

??注意一下,這裡在if語句中的一行代碼,if語句中是檢驗對MANIFEST.MF整體文件的簽名與[CERT].SF中記錄的是否一致。若一致,說明MANIFEST.MF沒有被修改,所以不必檢驗MANIFEST.MF剩下的條目。若不一致,說明MANIFEST.MF文件被修改,但是,從程序if分支中的代碼可以看到,程序並沒有立馬拋出異常,而是繼續檢驗MANIFEST.MF中的其他條目的hash和[CERT].SF中的記錄是否一致。

??一開始對這個算法還挺困惑的,既然檢測出了MANIFEST.MF被修改,為什麼不直接拋出SecurityException異常,而是繼續檢測MANIFEST.MF中的其他條目。想了一會兒,終於體會到Google工程師的編程的偉大了。我們看到,在檢測數MANIFEST.MF文件被修改後,由於MANIFEST.MF中的頭部已經通過檢驗。說明一定是MANIFEST.MF中的某個條目被修改了,於是,在while()循環中針對每個條目進行校驗時,一定不能通過。並且,通過invalidDigest()函數拋出異常。這樣做有什麼好處就是可以定位MANIFEST.MF哪個條目被修改(從而可以進一步確定apk中哪個文件被修改)。這一點我們可以通過invalidDigest()函數看出。

private static SecurityException invalidDigest(String signatureFile, String name, String jarName) {
    throw new SecurityException(signatureFile + " has invalid digest for " + name + " in " + jarName);
}

??好了,上面一直說檢驗MANIFEST.SF中的條目hash值與[CERT].SF中的值是否匹配,我們看一下到底到底怎麼檢測的,查看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;
}
private static final String[] DIGEST_ALGORITHMS = new String[] {
   "SHA-512",
   "SHA-384",
   "SHA-256",
   "SHA1",
};

??可以看到,有4中hash方法可供選擇,由於不知道apk簽名時采用了什麼hash算法,所以對4中算法進行遍歷,通過“算法名+傳入的entry名”的方式來確定使用了何種算法。例如,通過嘗試“SHA1-Digest”從[CERT].SF中取值來確定使用了何種算法,若取到的值為非空,說明采用的是SHA1算法,否則進行下一個嘗試。最後,將屬性值(具體來說就是MANIFEST.MF文件中對應條目的值)hash+Base64與傳入的[CERT].SF中的值比對,若結果相同返回true,否則返回false。參數ignorable表示這個驗證是否可以忽略,若這個值設置為true。當屬性值不存在是,依舊返回true。

??到此為止,StrictJarFile實例的構造過程實際上已經完成了簽名校驗的兩部分:一是對CERT.SF文件hash在與[CERT].RSA中的簽名值進行比對,保證[CERT].SF沒有被修改;二是對MANIFEST.MF文件中的各條目hash然後和[CERT].SF中各條目比對,確保MANIFEST.MF文件沒有被修改過。

??現在,我們繼續回到PackageParser.java分析collectCertificates()中調用的loadCertificates(jarFile, entry)留下的問題:verifiedEntries是怎樣被賦值的。於是我們回顧一下這一條函數調用鏈。

Created with Rapha?l 2.1.0PackageManagerService中:collectCertificates(Package pkg, int flags)PackageParser中:collectCertificates(Package pkg, File apkFile, int flags)PackageParser中:loadCertificates(StrictJarFile jarFile, ZipEntry entry)StrictJarFile中:getCertificateChains(ZipEntry ze) JarVerifier中:getCertificateChains(String name)上述函數內部:verifiedEntries.get(name);verifiedEntries怎麼實例化

??在上面流程圖,在PackageParser的loadCertificates()函數實現中,在調用getCertificateChains()函數前,還調用了另外兩行代碼。

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)throws PackageParserException {

       ````
      try {
            // We must read the stream for the JarEntry to retrieve
            // its certificates.
            is = jarFile.getInputStream(entry);
            readFullyIgnoringContents(is);
            return jarFile.getCertificateChains(entry);
        }

        ````
}

??我們在StrictJarFile.java中查看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;
}

??代碼很簡單,就調用了兩個函數,一個調用了JarVerifier.java中的initEntry()函數。二是調用了JarVerifier.java中的JarFileInputStream構造函數。我們首先查看initEntry()函數。

VerifierEntry initEntry(String name) {
    // If no manifest is present by the time an entry is found,
    // verification cannot occur. If no signature files have
    // been found, do not verify.
    if (manifest == null || signatures.isEmpty()) {
        return null;
    }

    Attributes attributes = manifest.getAttributes(name);
    // entry has no digest
    if (attributes == null) {
        return null;
    }

    ArrayList certChains = new ArrayList();
    Iterator>> it = signatures.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry> entry = it.next();
        HashMap 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對象,我們簡要分析一下VerifierEntry構造器的參數。VerifierEntry(String name, MessageDigest digest,byte[] hash,Certificate[][] certChains,Hashtable《String, Certificate[][]> verifedEntries)。第一個參數String類型,對應的就是要驗證的文件的文件名,第二參數是計算摘要時用到的方法的對象。同樣地,這裡也不知道用的是SHA1,SHA-256還是SHA-512,所以和前面一樣,也采用了一個for循環,嘗試從MANIFEST.MF文件中取“SHA1-Digest”條目。取到值說明是對應用到了對應的算法。第三個參數是從MANIFEST.MF文件中取到的條目。第四個參數是證書鏈,是一個二維數組(為什麼是二維數組呢?這是因為Android允許用多個證書對apk進行簽名,但是它們的證書文件名必須不同。)。這裡初始化第四個參數時注意一下,直接遍歷signatures,然後直接從每一項中取對應的certificates成員得到的證書鏈。

這裡寫圖片描述

??所以繼續看一下signatures和certificates成因的變量類型和初始化過程。

 

private final Hashtable> signatures =
        new Hashtable>(5);

private final Hashtable certificates =
        new Hashtable(5);

??在之前jarFile調用構造函數的過程中,其實已經對這兩個變量進行了初始化,這裡回顧一下。

private void verifyCertificate(String certFile) {

    ````
    String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";

    ````
    try {
       Certificate[] signerCertChain = JarUtils.verifySignature(
               new ByteArrayInputStream(sfBytes),
               new ByteArrayInputStream(sBlockBytes));
       if (signerCertChain != null) {
           certificates.put(signatureFile, signerCertChain);
       }
   }

   ````
  Attributes attributes = new Attributes();
  HashMap entries = new HashMap();
  try {
      ManifestReader im = new ManifestReader(sfBytes, attributes);
      im.readEntries(entries, null);
  } catch (IOException e) {
      return;
  }

  ````
   signatures.put(signatureFile, entries);
}

??可以看到,signatures其實保存的鍵值對是:HashTable<[CERT].SF文件名,[CERT].SF中各條目組成的HashMap>,而certificates實際上保存的是<[CERT].SF文件,證書文件數組>形成的HashTable。從上面的代碼看出,certificates的初始化又用到了JarUtils.verifySignature(new ByteArrayInputStream(sfBytes),new ByteArrayInputStream(sBlockBytes))得到證書鏈信息,鑒於不想篇幅過長,向前面說的,這部分留作一個思考,以後的Blog繼續討論。

??第五個參數是已經通過驗證的文件的HashTable。接下來分析JarFileInputStream,構造函數很簡單,沒啥好說的。

JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
    super(is);
    entry = e;

    count = size;
}

??把loadCertificates()中的函數路線在梳理一下,在調用完getInputStream()函數後,接著調用的是readFullyIgnoringContents()函數。

Created with Rapha?l 2.1.0loadCertificates()(1st) is=getInputStream(entry);initEntry()和JarFileInputStream()
Created with Rapha?l 2.1.0loadCertificates()(2nd)readFullyIgnoringContents(is);

??查看readFullyIgnoringContents()函數源碼,這個函數就是讀取InputStream的數據流,並統計讀取到的長度。

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;
}

??
??這裡的InputStream實際上是JarFileInputStream。查看其重載的read方法。

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();
        }
        return r;
    } else {
        done = true;
        entry.verify();
        return -1;
    }
}

??read()函數很簡單,除了讀取數據外,還調用了write()函數和verify()函數,下面分別查看這兩個函數的源碼。

public void write(byte[] buf, int off, int nbytes) {
    digest.update(buf, off, nbytes);
}

??write函數很簡單,就是將讀到的文件的內容傳給digest,這個digest就是前面在構造JarVerifier.VerifierEntry傳進來的,對應於在MANIFEST.MF文件中指定的摘要算法。

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

??到這個函數,一切變得明朗起來。這個函數首先計算apk中哥哥文件的摘要值,然後進行base64編碼,最後把計算出來的值和MANIFEST.MF文件中記錄的值進行比較,用以說明apk中的文件是否受到修改。若相同,說明受修改,拋出SecurityException異常。

 private static SecurityException invalidDigest(String signatureFile, String name,
         String jarName) {
     throw new SecurityException(signatureFile + " has invalid digest for " + name +
             " in " + jarName);
 }

??不要忘記,最上面的分析過程中還有一個問題遺留下來,就是關於JarVerifier中的成員verifiedEntries怎麼實例化的分析,這裡給出了答案。在verify()函數最後一行,對於校驗過得文件,會添加到verifiedEntries成員上。

??ok,整個源碼過程總算分析完了。這裡再整理一下從loadCertificates()到(2nd)readFullyIgnoringContents(is)最後verify()的函數調用鏈。

Created with Rapha?l 2.1.0loadCertificates()PackageParser中(2nd)readFullyIgnoringContents(is);JarFile中重載的read(byte[] buffer, int byteOffset, int byteCount)方法write(buffer, byteOffset, size)和verify();verify()對ap中的文件摘要與MANIFEST.MF對應的條目校驗;並對verifiedEntries初始化;

二、 總結

1. 簽名過程總結

??簽名過程沒有分析源碼,直接根據之前學習的內容總結。

??在apk中,/META-INF文件夾中保存著apk的簽名信息,一般至少包含三個文件,[CERT].RSA,[CERT].SF和MANIFEIST.MF文件。這三個文件就是對apk的簽名信息。

MANIFEST.MF中包含對apk中除了/META-INF文件夾外所有文件的簽名值,簽名方法是先SHA1()(或其他hash方法)在base64()。存儲形式是:Name加[SHA1]-Digest。 [CERT].SF是對MANIFEST.MF文件整體簽名以及其中各個條目的簽名。一般地,如果是使用工具簽名,還多包括一項。就是對MANIFEST.MF頭部信息的簽名,關於這一點前面源碼分析中已經提到。 [CERT].RSA包含用私鑰對[CERT].SF的簽名以及包含公鑰信息的數字證書。

??是否存在簽名偽造可能:

修改(含增刪改)了apk中的文件,則:校驗時計算出的文件的摘要值與MANIFEST.MF文件中的條目不匹配,失敗。 修改apk中的文件+MANIFEST.MF,則:MANIFEST.MF修改過的條目的摘要與[CERT].SF對應的條目不匹配,失敗。 修改apk中的文件+MANIFEST.MF+[CERT].SF,則:計算出的[CERT].SF簽名與[CERT].RSA中記錄的簽名值不匹配,失敗。 修改apk中的文件+MANIFEST.MF+[CERT].SF+[CERT].RSA,則:由於證書不可偽造,[CERT].RSA無法偽造。

??

2. 校驗過程總結

??根據App簽名校驗過程的源碼分析,校驗過程如下:

在初始化StrictJarFile實例時,在其構造器中調用了readCertificates()方法,隨後的函數調用鏈完成了兩個工作:一是對CERT.SF文件hash在與[CERT].RSA中的簽名值進行比對,保證[CERT].SF沒有被修改;二是對MANIFEST.MF文件中的各條目hash然後和[CERT].SF中各條目比對,確保MANIFEST.MF文件沒有被修改過。 在packageParser的loadCertificates()中調用了readFullyIgnoringContents()函數,隨後的函數調用鏈實現了對apk中文件簽名校驗的工作。具體來說,計算apk中文件的摘要值,然後將值與MANIFEST.MF文件中對應的條目進行比對,確保apk中的文件沒有被修改過。

3. 一個疑問

??在上面源碼分析過程中,丟下了一小點沒有分析,就是JarUtils.verifySignature( new ByteArrayInputStream(sfBytes), new ByteArrayInputStream(sBlockBytes))這個函數到底做啥的。還有就是證書鏈Certificate[]這個數據結構也沒有弄明白。姑且放下這些,這裡先提一個問題,上面總結1中提到的關系簽名偽造“由於證書不可偽造,[CERT].RSA無法偽造”,我就在想,既然校驗過程是將[CERT].SF計算簽名值,然後和[CERT].RSA中記錄的簽名值對比,而且在計算時是不可能知道私鑰信息的。那麼問題來了:為什麼不能讀取[CERT].RSA中的簽名值,然後做修改,使得其和計算的值匹配?換句話說,簽名校驗過程中,是怎麼利用公私鑰檢驗的,數字證書在檢驗函數中發揮的具體作用是啥?

??源碼分析中僅僅校驗上面說的幾個值是否匹配的問題,並沒有說明證書的作用。換句話說,對App換一個簽名是能夠通過校驗的。但是,在App升級時,需要驗證證書是否一致,而不是對應的值是都匹配,關於這一點,前面的源碼中沒有提到。帶著這些個疑問出發,後面繼續分析在App升級時,證書發揮的作用。感覺和verifySignature()這個函數的細節有一點關系,期待後面的分析。To you and myself!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved