Android PackageManager Service详解(5.1源码)(二)

1.2.3 APK数据读取和校验

 之前说过,APK其实就是一个ZIP格式的压缩包,由于读取APK数据时要做完整性验证和签名数据的提取,android修改了java标准SDK中的JarFile,JarEntry等文件的实现。

 下面是android第一次读取APK内数据文件的交互图:

介绍相关类以及成员变量

1)   JarFile,这个类大家都很熟悉,它内部跟校验相关的有如下两个变量

JarFile:Manifest manifest   MANIFEST.MF清单数据对象

JarFile:JarVerifier verifier   顾名思义,负责公钥证书的提取和完整性校验

2)   JarFileInputStream负责Jar指定文件的数据读取,在构造时,需要传入JarVerifier.VerifierEntry,用于在数据读取结束后对当前读取文件的完整性做校验

3)   JarVerifier.VerifierEntry对Jar指定项进行校验,构造时传入指定项名,摘要生成算法(SHA-1),hash值,以及公钥证书

 

接下去从代码角度,对上面的交互流程做一个完整的分析

首先,根据apk路径,构造JarFile对象

JarFile jarFile = newJarFile(“***.apk”);

JarFile的构造函数其实还可以传入是否需要做签名校验,默认是true

接着,我们以读取AndroidManifest.xml文件来举例

JarEntry jarEntry = jarFile.getJarEntry(“AndroidManifest.xml”);

   jarFile.getInputStream(jarEntry);

所有的一切都发生在getInputStream里,接下去看其代码

public InputStream getInputStream(ZipEntry ze) throws IOException {

        if (manifestBytes != null) {

            getManifest();

        }

 

        if (verifier != null) {

            if (verifier.readCertificates()) {

                verifier.removeMetaEntries();

                manifest.removeChunks();

 

                if (!verifier.isSignedJar()) {

                    verifier = null;

                }

            }

        }

 

        InputStream in = super.getInputStream(ze);

        if (in == null) {

            return null;

        }

        if (verifier == null || ze.getSize() == -1) {

            return in;

        }

        JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());

        if (entry == null) {

            return in;

        }

        return new JarFileInputStream(in, ze.getSize(), entry);

    }

这个函数在第一次调用时,会先通过getManifest初始化APK摘要清单数据(MANIFEST.MF)和调用JarVerifier.readCertificate获取证书数据和APK数据的完整性验证(CERF.SF,MANIFEST.MF).

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目录下的所有文件信息,它在JarVerifier构造时被初始化,通过遍历meatEntries目录找到.RSA文件,接着调用verifyCertificate并传入该文件做公钥证书提取和完整性校验:

 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[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);

        // Manifest entry is required for any verifications.

        if (manifest == null) {

            return;

        }

        byte[] sBlockBytes = metaEntries.get(certFile);

        try {

            Certificate[] signerCertChain = JarUtils.verifySignature(

                    new ByteArrayInputStream(sfBytes),

                    new ByteArrayInputStream(sBlockBytes));

 

            if (signerCertChain != null) {

                certificates.put(signatureFile, signerCertChain);

            }

        }} catch (IOException e) {

            return;

        } catch (GeneralSecurityException e) {

            throw failedVerification(jarName, signatureFile);

        }

 

        // Verify manifest hash in .sf file

        Attributes attributes = new Attributes();

        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();

        try {

            ManifestReader im = new ManifestReader(sfBytes, attributes);

            im.readEntries(entries, null);

        } catch (IOException e) {

            return;

        }

        // Use .SF to verify the whole manifest.

        String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";

        if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, false, false)) {

        }

        metaEntries.put(signatureFile, null);

        signatures.put(signatureFile, entries);

    }

我将完整性校验的核心代码段标红,sfBytes为CERT.SF文件数据,sBlockBytes为CERT.RSA文件数据,接着调用JarUtils.verifySignature进行校验,主要包含:

1:从sBlockBytes中提取公钥证书和CERT.SF摘要数据

2:用拿到的公钥证书将CERT.SF摘要数据解密

3:根据sfBytes生成摘要数据并步骤2解密后的数据进行比对,如果一致,则校验通过,否则校验失败,抛出异常

 

在公钥证书提取和完整性校验通过后,从代码上看,还会继续逐条校验MANIFEST.MF和CERT.SF的摘要数据。

获取.SF内部数据:

Attributes attributes = new Attributes();

        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();

        try {

            ManifestReader im = new ManifestReader(sfBytes, attributes);

            im.readEntries(entries, null);

        }

之前说过,.SF的文件内容是MANIFEST.MF内每一项数据的信息摘要,所以接下去,调用verify逐条进行校验:

if (!verify(entry.getValue(), "-Digest", manifest,

                        chunk.start, chunk.end, createdBySigntool, false)) {

                    throw invalidDigest(signatureFile, entry.getKey(), jarName);

}

同样的,校验失败,会抛出异常,上面两步都校验通过,readcertificates在算真正完成

最后,将CERF.SF的内部数据缓存以备后续使用

signatures.put(signatureFile,entries);

 

  readCertificates只是做了公钥证书的提取,CERT.SF和MANIFEST.MF文件的完整性校验,那其他文件的完整性如何验证?回过头来继续看getInputStream在readcertificates之后的代码

JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());

调用JarVerifier.initEntry,并传入JarEntry名,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<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()][]);

 

        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;

    }

 

这个函数主要做了:

1)   通过manifest.getAttributes(name);拿到读取项的清单数据

2)   然后遍历CERT.SF,如果找到读取项,则将初始化certChains数据

3)   attributes.getValue(algorithm+ "-Digest");从清单中获取改项的摘要值

4)   根据以上数据,初始化VerifierEntry对象

 

最后,在创建JarFileInputStream(in, ze, entry)实例的时候,将VerifierEntry实例传入,由于传入的VerifierEntry对象已经包含欲读取项在MANIFEST.MF清单中的摘要数据,接下去,只需要在JarFileInputStream读取结束后,调用VerifierEntry.verifiy来做完整性校验即可。

 

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中供最终校验

 public void write(byte[] buf, int off, int nbytes) {

            digest.update(buf, off, nbytes);

 }

 

等数据全部读取完后,调用entry.verify:

void verify() {

            byte[] d = digest.digest();

            if (!MessageDigest.isEqual(d, Base64.decode(hash))) {

                throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName);

            }

            verifiedEntries.put(name, certificates);

}

校验失败,就抛出异常,如果成功则将该读取项名和证书数据缓存到verifiedEntries,上面

描述VerifierEntry的初始化时说过certificates其实保存的就是CERT.RSA的公钥证书,所以,对APK的所有文件来说,verifiedEntries保存的证书数据都是一样一样的。

1.2.4PMS中签名相关代码介绍

由于JarFile内部对证书的提取和校验都做了封装,所以PMS只需要像读普通Jar文件来读取APK即可,只要读取过程中没抛出异常,就说明数据是完整的

下面是PMS中收集签名证书的代码:

  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<ZipEntry> toVerify = new ArrayList<>();

            toVerify.add(manifestEntry);

 

            // If we're parsing an untrusted package, verify all contents

            if ((flags & PARSE_IS_SYSTEM) == 0) {

                final Iterator<ZipEntry> i = jarFile.iterator();

                while (i.hasNext()) {

                    final ZipEntry entry = i.next();

 

                    if (entry.isDirectory()) continue;

                    if (entry.getName().startsWith("META-INF/")) continue;

                    if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;

 

                    toVerify.add(entry);

                }

            }

 

            // Verify that entries are signed consistently with the first entry

            // we encountered. Note that for splits, certificates may have

            // already been populated during an earlier parse of a base APK.

            for (ZipEntry entry : toVerify) {

                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);

                if (ArrayUtils.isEmpty(entryCerts)) {

                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,

                            "Package " + apkPath + " has no certificates at entry "

                            + entry.getName());

                }

                final Signature[] entrySignatures = convertToSignatures(entryCerts);

 

                if (pkg.mCertificates == null) {

                    pkg.mCertificates = entryCerts;

                    pkg.mSignatures = entrySignatures;

                    pkg.mSigningKeys = new ArraySet<PublicKey>();

                    for (int i=0; i < entryCerts.length; i++) {

                        pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());

                    }

                } else {

                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {

                        throw new PackageParserException(

                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath

                                        + " has mismatched certificates at entry "

                                        + entry.getName());

                    }

                }

            }

        } catch (GeneralSecurityException e) {

            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,

                    "Failed to collect certificates from " + apkPath, e);

        } catch (IOException | RuntimeException e) {

            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,

                    "Failed to collect certificates from " + apkPath, e);

        } finally {

            closeQuietly(jarFile);

        }

    }

 

collectCertificates主要是对APK内部数据的完整性做验证并保存签名数据,之前说过,JarFileInputStream在读取结束,会对文件内容做完整性校验的,所以如果要验证整个apk内部文件是否是完整,只需要将全部文件挨个读取一遍,如果没有异常,就说明是ok的

代码介绍:

1)尝试读取AndroidManifest.xml文件,如果不存在,直接报错

2)如果存在,将AndroidManifest.xml加入校验列表

3)如果是系统应用,则只校验AndroidManifest.xml即可,否则需全部校验除META-INF目录以外的所有文件

4)校验成功后保存签名数据

final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);

看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(is);就是从inputstream读取全部数据,顺利读取完,说明没问题,并且公钥证书数据已经被缓存到verifiedEntries中,接着调用getCertificateChains读取证书数据:

public Certificate[][] getCertificateChains(ZipEntry ze) {

        if (isSigned) {

            return verifier.getCertificateChains(ze.getName());

        }

 

        return null;

    }

最终调用的是JarVerifier的成员函数

Certificate[][] getCertificateChains(String name) {

        return verifiedEntries.get(name);

    }

看到没有,直接从verifiedEntries中获取对应项的公钥证书数据,接着将证书数据保存

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

}

 

Apk在做签名比对的时候,一般都是基于pkg.mSignatures来进行的,它是公钥证书的encoded数据

开头说过,在执行APK签名时需要通过password和alias name从.keystore文件中提取公私钥对,所以通过公钥证书数据来确认发行者身份是没问题的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值