1、本文主要内容
- 知识回顾
- 签名验证解析
- 总结
本文介绍下Android在安装apk时,对签名的验证过程
2、知识回顾
在Android签名过程详解一文中,我已经详细说明签名的过程以及为什么要这么做,一起回顾下,当签名完成后,生成的3个文件分别有什么作用:
- 对Apk中的每个文件做一次算法(数据摘要+Base64编码),保存到MANIFEST.MF文件中
- 对MANIFEST.MF整个文件做一次算法(数据摘要+Base64编码),存放到CERT.SF文件的头属性中,在对MANIFEST.MF文件中各个属性块做一次算法(数据摘要+Base64编码),存到到一个属性块中。
- 对CERT.SF文件做签名,内容存档到CERT.RSA中
让我们带着这些基础知识来看看签名的验证过程
3、签名验证解析
Android Apk安装过程解析一文中阐述了apk安装过程,其中在 installPackageLI 方法中将验证签名。
//收集签名并验证
try {
pp.collectCertificates(pkg, parseFlags);
} catch (PackageParserException e) {
res.setError("Failed collect during installPackageLI", e);
return;
}
跟踪collectCertificates方法,它在 PackageParser 类当中:
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
//验证apk有没有androidmenifest.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);
// If we're parsing an untrusted package, verify all contents
//如果是不被信任的应用,那么将apk中除文件夹、META-INF文件夹内的文件、androidmenifest.xml的其它文件entry都添加到要验证的列表中
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.
//验证每一个entry
for (ZipEntry entry : toVerify) {
//loadCertificates方法中将验证MANIFEST.MF文件以及CERT.SF
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 {
//签名对比,相当于验证CERT.RSA中文件的内容
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);
}
}
方法中存在一个列表,toVerify ,待验证的列表,然后通过循环将apk中除文件夹、META-INF文件夹内的文件以及androidmenifest.xml以外的文件都添加到此列表中来,以上代码是否感觉非常熟悉?
// If we're parsing an untrusted package, verify all contents
//如果是不被信任的应用,那么将apk中除文件夹、META-INF文件夹内的文件、androidmenifest.xml的其它文件entry都添加到要验证的列表中
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);
}
}
是的,MANIFEST.MF文件中正是保存着Apk中的每个文件做一次算法(数据摘要+Base64编码),当然文件夹不做数据摘要,二者的对象非常类似,实际上 toVerify 列表正是为了验证 MANIFEST.MF文件,继续跟踪 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);
}
}
loadCertificates 方法中有两个参数,一个是代表apk的文件,另一个是代表apk中某一文件的entry,继续跟踪 getInputStream 方法
public InputStream getInputStream(ZipEntry ze) {
final InputStream is = getZipInputStream(ze);
if (isSigned) {
StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
if (entry == null) {
return is;
}
return new JarFileInputStream(is, ze.getSize(), entry);
}
return is;
}
方法内初始化了一个VerifierEntry 对象,并且构造了一个JarFileInputStream的输入流。StrictJarVerifier对象在StrictJarFile的构造方法中初始化的
//metaEntries 中包含了META-INF文件夹下三个文件读入的byte数组
HashMap<String, byte[]> metaEntries = getMetaEntries();
//manifest 代表着apk中MANIFEST.MF这个文件的读入结果
this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
this.verifier = new StrictJarVerifier(
name,
manifest,
metaEntries,
signatureSchemeRollbackProtectionsEnforced);
//读取META-INF下的三个文件,并且把读取结果保存在metaEntries对象中
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;
}
StrictJarVerifier的构造函数中有4个变量,第1个是apk的路径,第2个是包含MANIFEST.MF内容的数据结构,第3个是META-INF下的三个文件的读取结果,记住这3个参数。
回到verifier.initEntry函数
//name是要验证的其中一个entry的名字
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.
//如果没有manifest则返回
if (manifest == null || signatures.isEmpty()) {
return null;
}
//获取MANIFEST.MF中代表这个文件的数据摘要值
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()][]);
//真正开始验证MANIFEST.MF文件了
for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
final String algorithm = DIGEST_ALGORITHMS[i];
//从MANIFEST.MF文件中获取这个文件的数据摘要值
final String hash = attributes.getValue(algorithm + "-Digest");
if (hash == null) {
continue;
}
byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
try {
//VerifierEntry的name参数为要验证文件的名字,计算数据摘要的对象,MANIFEST.MF文件中获取这个文件的数据摘要值
//以及两个与签名相关的内容
return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
certChainsArray, verifiedEntries);
} catch (NoSuchAlgorithmException ignored) {
}
}
return null;
}
注意,manifest 对象前文已经说过了,是一个包含着MANIFEST.MF文件内容的数据结构,所以在initEntry中,要验证的文件的数据摘要值将被manifest 查找出来:
final String hash = attributes.getValue(algorithm + "-Digest");
algorithm 这是数据摘要采用的算法名,它是这么定义的:
private static final String[] DIGEST_ALGORITHMS = new String[] {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA1",
};
我们解压一个示例apk,发现MANIFEST.MF文件组织的形式如下,数据摘要的算法名是SHA1-Digest。和代码相比对一下发现,二者是吻合的,数据摘要将被读取出来
最后构建了一个VerifierEntry,传入的参数有,name参数为要验证文件的名字,计算数据摘要的对象,MANIFEST.MF文件中获取这个文件的数据摘要值以及两个与签名相关的内容。
回到 getInputStream 方法,此处以VerifierEntry为参数,返回了一个 JarFileInputStream 对象
return new JarFileInputStream(is, ze.getSize(), entry);
再返回到 PackageParser 的 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);
}
}
查看 readFullyIgnoringContents 方法
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;
}
这个方法也没有什么特殊的,就是读文件而已,但不要忘了,传入的输入流对象是 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;
}
//将文件 写入 VerifierEntry 中,以便为这个文件计算数据摘要值
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;
}
}
在读入数据的同时,会调用 entry.write,将数据写入到 VerifierEntry 当中,并且最后调用 entry.verify 方法。
private final MessageDigest digest;
public void write(byte[] buf, int off, int nbytes) {
digest.update(buf, off, nbytes);
}
void verify() {
byte[] d = digest.digest();
if (!verifyMessageDigest(d, hash)) {
throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
}
verifiedEntries.put(name, certChains);
}
private static boolean verifyMessageDigest(byte[] expected, byte[] encodedActual) {
byte[] actual;
try {
actual = java.util.Base64.getDecoder().decode(encodedActual);
} catch (IllegalArgumentException e) {
return false;
}
return MessageDigest.isEqual(expected, actual);
}
从上述代码可以看出,VerifierEntry 写入文件数据后,会使用 MessageDigest 对象重新计算文件的数据摘要值,并且和之前已经传入进来的数据摘要值进行比对,如果一致就是正确的,至此,MANIFEST.MF文件验证完成。
接下来看对CERT.SF文件的校验。
jarFile.getCertificateChains(entry);
public Certificate[][] getCertificateChains(ZipEntry ze) {
if (isSigned) {
return verifier.getCertificateChains(ze.getName());
}
return null;
}
看起来只是从列表中返回一个证书对象即可,但这些证书对象在哪里读取的呢?它是在StrictJarVerifier初始化时读取的
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;
}
值得注意的是,metaEntries在前文中已经说过,它是META-INF文件夹下3个文件使用输入流读取后生成的byte数组,在循环中,key明显就是这3个文件的文件名。显然只有文件名等于CERT.RSA方法,才能进入verifyCertificate方法
查看verifyCertificate方法
private void verifyCertificate(String certFile) {
// Found Digital Sig, .SF should already have been read
// 此前分析过,certFile只可能为CERT.RSA,所以signatureFile通过字符串拼接后是CERT.SF
String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
// sfBytes,即是CERT.SF文件的内容
byte[] sfBytes = metaEntries.get(signatureFile);
if (sfBytes == null) {
return;
}
// manifestBytes显然是MANIFEST.MF文件的内容
byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
// Manifest entry is required for any verifications.
if (manifestBytes == null) {
return;
}
// sBlockBytes显然是CERT.RSA文件的内容
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 {
// 使用StrictJarManifestReader读取CERT.SF文件的内容,并且把数据保存到attributes当中来
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;
//查看CERT.SF文件,查看Created-By属性,是否包含signtool关键字
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.
//这是针对以前的java版本在1.5之前的操作,现在忽略它
//而且verify方法的最后一个参数为false,verfy方法返回的就是最后一个参数的值,
//所以如果CERT.SF文件中没有-Digest-Manifest-Main-Attributes这个属性也不用担心
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.
//验证MANIFEST.MF整体文件的数据摘要
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;
}
//验证MANIFEST.MF其它条目的数据摘要值和自己计算的相比,是不是一样的
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);
}
verifyCertificate方法中,分别读取了代表CERT.SF、MANIFEST.MF和CERT.RSA这三个文件的内容,第1步,是从CERT.SF和CERT.RSA读取出签名来。接下来就是验证MANIFEST.MF整体的数据摘要内容以及MANIFEST.MF内部条目的数据摘要内容。
值得注意的是
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中,开头都不会包含-Digest-Manifest-Main-Attributes这个了,从英文注释上来讲只有java1.5版本以前会有这样的信息,所以这段代码可忽略。真正验证 MANIFEST.MF 整体文件数据摘要的是这段代码:
String digestAttribute = createdBySigntool ? "-Digest"
: "-Digest-Manifest";
if (!verify(attributes, digestAttribute, manifestBytes, 0,
manifestBytes.length, false, false)) {
我们知道,createdBySigntool 值为false,所以 digestAttribute 的值一定是 -Digest-Manifest。我们可以看看示例apk的CERT.SF文件内容
整体文件的算法为SHA1,后缀为 -Digest-Manifest ,和代码一致。
而后续则是验证 MANIFEST.MF 其它条目的数据摘要了,它使用的算法后缀是-Digest,我们从上图依然可以得到验证。
接下来我们看看verify方法,它的主要功能是对比两个数据摘要的内容:
- CERT.SF文件中记录着的数据摘要值
- 从MANIFEST.MF文件中读取文件并重新计算数据摘要值
//Attributes是包含CERT.SF文件内容的数据结构,entry则是数据摘要算法的后缀,data传入的是MANIFEST.MF文件的二进制读入流
//start和和end表示对data读取多个长度
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];
//遍历数据摘要算法,再拼接上后缀,看看能否从CERT.SF读取到这个条目的值,结果不为空则算法匹配上了
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);
//看看重新计算的数据摘要值和从CERT.SF读取出来的,是否一致
return MessageDigest.isEqual(b, Base64.decode(hashBytes));
}
return ignorable;
}
要特别留意verify方法的参数,把参数弄明白了基本上整个方法也都明白了。verify方法还是比较简单的,把两种方式得到的数据摘要值比较一下,如果相等就返回true了。
到目前为止,CERT.SF文件也已经验证完成了,那么CERT.RSA是在哪里验证的呢?
PackageParser类的collectCertificates方法会比对一次公钥信息,这其实就是在验证签名是不是原作者的签名,如果不对也会抛出异常:
if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
+ " has mismatched certificates at entry "
+ entry.getName());
}
总结
所以,apk的签名校验,通过以上3个步骤,就确保了apk自己的安全性,无法被篡改。
网友评论