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

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

    ````

}

Android App签名(证书)校验过程源码分析_第1张图片

Android App签名(证书)校验过程源码分析_第2张图片

  这里首先判断是否由工具签名,判断方法是根据[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成员得到的证书链。

Android App签名(证书)校验过程源码分析_第3张图片

  所以继续看一下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!

你可能感兴趣的:(Android安全)