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 contentsif ((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 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 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);         }     }    ````}



  这里首先判断是否由工具签名,判断方法是根据[CERT].SF文件中的Created-By条目中是否由signtool关键字,若有,说明是工具签名,则检验MANIFEST.MF文件的头部的hash与[CERT].SF中记录的条目SHA1-Digest-Manifest-Main-Attributes: KdSJo1gAKJkR4HRZDprFCj1n3S4=是否匹配。接着,就是检验MANIFEST.MF中的所有条目的hash值与[CERT].SF中所记录的对应条目是否匹配。若不匹配,说明MANIFET.MF文件遭到修改。

        // Use .SF to verify the whole manifest.String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {    Iterator> it = entries.entrySet().iterator();    while (it.hasNext()) {        Map.Entry entry = it.next();        Manifest.Chunk chunk = manifest.getChunk(entry.getKey());        if (chunk == null) {            return;        }        if (!verify(entry.getValue(), "-Digest", manifestBytes,                chunk.start, chunk.end, createdBySigntool, false)) {            throw invalidDigest(signatureFile, entry.getKey(), jarName);        }    }}metaEntries.put(signatureFile, null);signatures.put(signatureFile, entries);

  注意一下,这里在if语句中的一行代码,if语句中是检验对MANIFEST.MF整体文件的签名与[CERT].SF中记录的是否一致。若一致,说明MANIFEST.MF没有被修改,所以不必检验MANIFEST.MF剩下的条目。若不一致,说明MANIFEST.MF文件被修改,但是,从程序if分支中的代码可以看到,程序并没有立马抛出异常,而是继续检验MANIFEST.MF中的其他条目的hash和[CERT].SF中的记录是否一致。

  一开始对这个算法还挺困惑的,既然检测出了MANIFEST.MF被修改,为什么不直接抛出SecurityException异常,而是继续检测MANIFEST.MF中的其他条目。想了一会儿,终于体会到Google工程师的编程的伟大了。我们看到,在检测数MANIFEST.MF文件被修改后,由于MANIFEST.MF中的头部已经通过检验。说明一定是MANIFEST.MF中的某个条目被修改了,于是,在while()循环中针对每个条目进行校验时,一定不能通过。并且,通过invalidDigest()函数抛出异常。这样做有什么好处就是可以定位MANIFEST.MF哪个条目被修改(从而可以进一步确定apk中哪个文件被修改)。这一点我们可以通过invalidDigest()函数看出。

private static SecurityException invalidDigest(String signatureFile, String name, String jarName) {    throw new SecurityException(signatureFile + " has invalid digest for " + name + " in " + jarName);}

  好了,上面一直说检验MANIFEST.SF中的条目hash值与[CERT].SF中的值是否匹配,我们看一下到底到底怎么检测的,查看verify()函数源码。

private boolean verify(Attributes attributes, String entry, byte[] data,        int start, int end, boolean ignoreSecondEndline, boolean ignorable) {    for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {        String algorithm = DIGEST_ALGORITHMS[i];        String hash = attributes.getValue(algorithm + entry);        if (hash == null) {            continue;        }        MessageDigest md;        try {            md = MessageDigest.getInstance(algorithm);        } catch (NoSuchAlgorithmException e) {            continue;        }        if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {            md.update(data, start, end - 1 - start);        } else {            md.update(data, start, end - start);        }        byte[] b = md.digest();        byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);        return MessageDigest.isEqual(b, Base64.decode(hashBytes));    }    return ignorable;}
private static final String[] DIGEST_ALGORITHMS = new String[] {   "SHA-512",   "SHA-384",   "SHA-256",   "SHA1",};

  可以看到,有4中hash方法可供选择,由于不知道apk签名时采用了什么hash算法,所以对4中算法进行遍历,通过“算法名+传入的entry名”的方式来确定使用了何种算法。例如,通过尝试“SHA1-Digest”从[CERT].SF中取值来确定使用了何种算法,若取到的值为非空,说明采用的是SHA1算法,否则进行下一个尝试。最后,将属性值(具体来说就是MANIFEST.MF文件中对应条目的值)hash+Base64与传入的[CERT].SF中的值比对,若结果相同返回true,否则返回false。参数ignorable表示这个验证是否可以忽略,若这个值设置为true。当属性值不存在是,依旧返回true。

  到此为止,StrictJarFile实例的构造过程实际上已经完成了签名校验的两部分:一是对CERT.SF文件hash在与[CERT].RSA中的签名值进行比对,保证[CERT].SF没有被修改;二是对MANIFEST.MF文件中的各条目hash然后和[CERT].SF中各条目比对,确保MANIFEST.MF文件没有被修改过。

  现在,我们继续回到PackageParser.java分析collectCertificates()中调用的loadCertificates(jarFile, entry)留下的问题:verifiedEntries是怎样被赋值的。于是我们回顾一下这一条函数调用链。

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 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成员得到的证书链。


  所以继续看一下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()函数。

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!

更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. Android(安卓)NDK系列一(ndk在android studio基本编译配置 ndk-bu
  6. Android(安卓)利用OnDraw实现自定义View
  7. android顶部(toolbar)搜索框实现
  8. [置顶] 自己开发的Android(安卓)软件发布贴(11月6日)
  9. Android(安卓)Studio构建工具Gradle构建原理

随机推荐

  1. 命令行装android
  2. Android构建工具
  3. Android API Level对应Android版本一览表
  4. android 欢迎画面
  5. Linux下Android开发平台的搭建
  6. 2011.07.08(5)——— android shortcut
  7. android 通过数组,流播放声音的方法
  8. Android 控件的可见,不可见,隐藏的设置【
  9. 搜索栏+流式布局+数据库
  10. android 获取手机的信息