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 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.
HashMapbyte[]> 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 HashMapbyte[]> getMetaEntries() throws IOException {
HashMapbyte[]> metaEntries = new HashMapbyte[]>();
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, HashMapbyte[]> 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);
}
}
````
}
// 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;
}
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成员得到的证书链。
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()函数。
查看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的签名信息。
是否存在签名伪造可能:
根据App签名校验过程的源码分析,校验过程如下:
在上面源码分析过程中,丢下了一小点没有分析,就是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!