編輯:關於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 ListtoVerify = 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 ListtoVerify = 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 Iteratori = 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 HashtableverifiedEntries=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. HashMapmetaEntries = 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, HashMapmetaEntries) { 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; } Iteratorit = 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(); HashMapentries = 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); } } ```` }
<喎?/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是怎樣被賦值的。於是我們回顧一下這一條函數調用鏈。
??在上面流程圖,在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; } ArrayListcertChains = 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成員得到的證書鏈。
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(); HashMapentries = 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()函數。
??查看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()的函數調用鏈。
??簽名過程沒有分析源碼,直接根據之前學習的內容總結。
??在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無法偽造。??
??根據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中的文件沒有被修改過。??在上面源碼分析過程中,丟下了一小點沒有分析,就是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!
異步消息處理線程是指線程啟動後會進入一個無限循環,每循環一次,從內部的消息隊列裡面取出一個消息,並回調相應的消息處理函數。一般在任務常駐,比如用戶交互任務的情況下使用異步
單例模式,可以說是GOF的23種設計模式中最簡單的一個。這個模式相對於其他幾個模式比較獨立,它只負責控制自己的實例化數量單一(而不是考慮為用戶產生什麼樣的實例
ZXing是谷歌的一個開源庫,可以用來生成二維碼、掃描二維碼。本文所介紹的是第一部分。首先上效果圖:ZXing相關各種文件官方下載地址:https://github.co
從系統相冊中選擇照片或則調用系統相機。大部分的項目都會用到,我進行了一下封裝,仿ios的效果。效果如下:1、Camera的基類package com.zhang.test