Android App签名(证书)校验过程源码分析
来源:互联网 发布:卓越之剑全套完整源码 编辑:程序博客网 时间:2024/05/21 22:41
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的签名校验过程,分析完源码之后,也会和网上大多数的资料一样给出总结。
注意:由于签名校验过程是在App安装时进行的,所以源码分析的起始点是上篇Blog:PackageInstaller源码分析。不过不想了解PackageInstall源码也没有关系,只要不纠结程序的起点,分析过程就是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<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); }}
在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<ZipEntry> toVerify = new ArrayList<>();toVerify.add(manifestEntry);
然后,程序遍历apk文件的所有文件节点,把除了META-INF/文件夹里面的文外外的所以文件加入待检验List。
// If we're parsing an untrusted package, verify all contentsif ((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); }}
紧接着把所以节点传入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<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()); } }}
要知道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<String, Certificate[][]> verifiedEntries=new Hashtable<String, Certificate[][]>();
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<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");}private HashMap<String, byte[]> getMetaEntries() throws IOException { HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(); Iterator<ZipEntry> 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<String, byte[]> 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<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;}
这个函数从META-INF/文件夹中提取以.DSA或.RSA或.EC结尾的文件,然后交给verifyCertificate(key)函数处理。所以我们查看verifyCertificate(key)函数实现。
private void verifyCertificate(String certFile) {// Found Digital Sig, .SF should already have been readString 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 fileAttributes 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);}
这个方法中,首先提取[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); } } ````}
这里首先判断是否由工具签名,判断方法是根据[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<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);
注意一下,这里在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; } 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对象,我们简要分析一下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<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>(5);private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>(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<String, Attributes> entries = new HashMap<String, Attributes>(); 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()的函数调用链。
二、 总结
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!
- Android App签名(证书)校验过程源码分析
- Android App运行时签名校验
- Android签名与校验过程详解
- 关于Android签名证书的生成过程
- android程序中证书签名校验的方法一
- Android5.1.1 - APK签名校验分析和修改源码绕过签名校验
- Android5.1.1-APK签名校验分析和修改源码绕过签名校验
- Android应用程序签名过程分析
- Android应用程序签名过程分析
- APK签名校验分析
- Android APP之WebView如何校验SSL证书
- 如何发布android 应用程序,app增加签名证书
- Android App实现自签名的 SSL 证书
- app运行时签名校验
- Android签名与签名校验
- Android APK 签名校验
- Android签名证书
- 查看android证书签名
- explicit
- Storm上的Nimbus、Supervisor以及Worker之间的关系
- git 恢复删除的所有文件 (含中文)
- Hibrenate hibernate query language(单表操作)
- 乐视三面总结
- Android App签名(证书)校验过程源码分析
- connect nonblock mode
- 标准体重的测量
- 寄存器
- eclipse color theme chose
- Oracle在Windows cmd上显示调整
- 常见排序算法及对应的时间复杂度和空间复杂度
- python笔试题之找出一个列表里出现频次最高的元素(most common elements in a list)
- leetcode 32.Longest Valid Parentheses