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

  紧接着把所以节点传入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 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<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是怎样被赋值的。于是我们回顾一下这一条函数调用链。

Created with Raphaël 2.1.0 PackageManagerService中: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 digestif (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 fileString signatureFile = entry.getKey();Certificate[] certChain = certificates.get(signatureFile);if (certChain != null) {certChains.add(certChain);}}}// entry is not signedif (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()函数。

Created with Raphaël 2.1.0 loadCertificates() (1st) is=getInputStream(entry); initEntry()和JarFileInputStream()

Created with Raphaël 2.1.0 loadCertificates() (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.0 loadCertificates() 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!

Android App签名(证书)校验过程源码分析相关推荐

  1. Android应用程序启动Binder线程源码分析

    Android的应用程序包括Java应用及本地应用,Java应用运行在davik虚拟机中,由zygote进程来创建启动,而本地服务应用在Android系统启动时,通过配置init.rc文件来由Init ...

  2. Android—OkHttp同步异步请求过程源码分析与拦截器

    OkHttp同步请求步骤: 创建OkHttpClient,客户对象 创建Request,请求主体,在请求主体设置请求的url,超时时间等 用newCall(request)将Reuqest对象封装成C ...

  3. Android V1签名与校验原理分析(全网最全最详细)

    [前言] Android Apk V1签名方式是一开始时使用的签名方案,不过V1签名方式也称作Jar签名,顾名思义,就是V1签名并不是Android独有的签名方式,而且在Android还没出来时候,J ...

  4. Android App签名的那些事

    App签名 Android App签名的目的是确保App的安装包来自于原创的作者,且App没有被篡改.Android手机是如何设别App来自于原创的作者且没有被篡改呢?请看App签名以及验签原理. A ...

  5. Activity启动流程源码分析(基于Android N)

    Activity启动流程源码分析 一个Activity启动分为两种启动方式,一种是从Launcher界面上的图标点击启动,另一种是从一个Activity中设置按钮点击启动另外一个Activity.这里 ...

  6. Android系统默认Home应用程序(Launcher)的启动过程源码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还须要有一个Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home应 ...

  7. Android 数据Parcel序列化过程源码分析

    在Android系统中,所有的服务都必须注册到ServiceManger中,当客户进程需要请求某一服务时,首先从服务管家ServiceManger中查找出该服务,然后通过RPC远程调用的方式使用该服务 ...

  8. Android服务查询完整过程源码分析

    Android服务注册完整过程源码分析中从上到下详细分析了Android系统的服务注册过程,本文同样针对AudioService服务来介绍Android服务的查询过程. 客户端进程数据发送过程 pri ...

  9. Android服务注册完整过程源码分析

    前面从不同片段分析了Android的Binder通信机制,本文结合前面介绍的内容,对整个Android的Binder通信过程进行一次完整的分析.分析以AudioService服务的注册过程为例. 由于 ...

最新文章

  1. C算法编程题(四)上三角
  2. dynamic和var的区别
  3. mysql group_concat去重_mysql 数据库group_concat函数的一些用法
  4. I/0口输入输出实验 流水灯程序 P0、P1、P2、P3口作为输出口,连接八只发光二极管,编写程序,使发光二极管从左至右循环点亮。
  5. 企业邮箱及邮件服务器架设
  6. sm总线控制器找不到驱动程序_细说嵌入式系统下的驱动程序设计
  7. 佳铁精雕机连接电脑设置_佳铁精雕机在程式里怎么更改G57之后的坐标
  8. hadoop环境搭建总结
  9. 计算机类中文核心期刊
  10. MYSQL命令行闪退问题解决
  11. 洛伦茨曲线_洛伦兹曲线
  12. 苹果手机上网速度慢_手机为什么下载速度很慢(揭晓手机下载速度慢的原因)...
  13. UOJ#449 喂鸽子
  14. 自动驾驶中的多传感器融合
  15. servlet:共享资源造成的线程冲突
  16. 在WORD中批量修改图片大小
  17. 《脚本》Python在线百度文库爬虫(免下载券)
  18. autoCAD数据库读写
  19. 晶晨905 2G+16G 与 1G+8G的内存占用对比
  20. 【java】课程设计--抽卡模拟器

热门文章

  1. 基于MFC的五子棋设计与实现
  2. IntellIdea 注册码
  3. 【web】HTTP(s)协议详解(重点:HTTPS 的加密过程浏览器中输入网址后,发生了什么?)
  4. 无人驾驶仿真软件PanoSim:(1)介绍
  5. Linux开发工具(3)——gcc/g++
  6. IntelliJ Idea 剪切板的复制深度设置(默认是五5条,用着很不爽!)
  7. Johnson-Trotter算法求全排列
  8. matlab的tfdata函数_MATLAB 主要函数指令表(按功能分类)
  9. ppt太大发不了邮件怎么办?
  10. MySQL DBA的修炼与未来