Android签名验证解析

作者: 某昆 | 来源:发表于2018-07-01 15:37 被阅读48次

    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自己的安全性,无法被篡改。

    相关文章

      网友评论

        本文标题:Android签名验证解析

        本文链接:https://www.haomeiwen.com/subject/iwvcuftx.html