也谈Android签名机制

284 阅读14分钟
原文链接: www.jianshu.com

1. 前言

关于Android的签名机制,在一个月前就看过了,当时还写了下流程,感觉没有太大的技术含量就没有记录。最近在看APK安装过程,突然又想起安装过程包含了APK的验证,关于APK的验证无非就是签名的逆过程。但是发现自己对签名过程好像模糊了很多,遂决定记录下签名过程。

2. 关于签名

Android的签名现在分为两个版本:v1和v2,因为v1版本签名过程的缺陷,造成了APK可能被攻击
v1签名:签名和摘要文件为APK解压后的META-INF文件夹下的*.MF、*.SF、*.RSA文件,其签名过程需要对文件进行解压并且计算每个文件的摘要。
v2签名:签名信息存储在ZIP文件格式中。7.0以上支持,7.0以下不支持,只能采用v1签名。

v2签名块位置

3. v1签名源码

我用到的源码都是在在线源码网站上下载的,这里用到了SignApk.java文件。
我们都知道如果使用命令行签名的话,都是执行的main方法:

public static void main(String[] args) {

    // 对输入参数的解析和验证
    ......
    boolean signWholeFile = false;
    String providerClass = null;
    int alignment = 4;
    int minSdkVersion = 0;
    boolean signUsingApkSignatureSchemeV2 = true;

    int argstart = 0;
    // 对输入参数的解析和验证
    ......

    loadProviderIfNecessary(providerClass);

    String inputFilename = args[args.length - 2];
    String outputFilename = args[args.length - 1];

    JarFile inputJar = null;
    FileOutputStream outputFile = null;
    int hashes = 0;

    try {
        // 公钥文件
        File firstPublicKeyFile = new File(args[argstart + 0]);

        X509Certificate[] publicKey = new X509Certificate[numKeys];
        ......
        long timestamp = 1230768000000L;
        timestamp -= TimeZone.getDefault().getOffset(timestamp);
        // 私钥文件
        PrivateKey[] privateKey = new PrivateKey[numKeys];
        for (int i = 0; i < numKeys; ++i) {
            int argNum = argstart + i * 2 + 1;
            privateKey[i] = readPrivateKey(new File(args[argNum]));
        }
        
        // 输入的文件
        inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
        // 输出文件
        outputFile = new FileOutputStream(outputFilename);
        
        // 直接对整个文件签名,这里不看
        if (signWholeFile) {
            SignApk.signWholeFile(inputJar, firstPublicKeyFile,
                    publicKey[0], privateKey[0],
                    timestamp, minSdkVersion,
                    outputFile);
        } else {
            ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
            JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
            outputJar.setLevel(9);
            // 1. 生成.MF信息内容
            Manifest manifest = addDigestsToManifest(inputJar, hashes);
            copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
            // 2. 对文件签名,生成.SF文件内容并且签名
            signFile(
                    manifest,
                    publicKey, privateKey,
                    timestamp, minSdkVersion, signUsingApkSignatureSchemeV2,
                    outputJar);
            outputJar.close();
            ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
            v1SignedApkBuf.reset();

            ByteBuffer[] outputChunks;
            // 使用v2签名
            if (signUsingApkSignatureSchemeV2) {
                // Additionally sign the APK using the APK Signature Scheme v2.
                ByteBuffer apkContents = v1SignedApk;
                List<ApkSignerV2.SignerConfig> signerConfigs =
                        createV2SignerConfigs(
                                privateKey,
                                publicKey,
                                new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
                outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
            } else {
                // Output the JAR-signed APK as is.
                outputChunks = new ByteBuffer[]{v1SignedApk};
            }

            // This assumes outputChunks are array-backed. To avoid this assumption, the
            // code could be rewritten to use FileChannel.
            for (ByteBuffer outputChunk : outputChunks) {
                outputFile.write(
                        outputChunk.array(),
                        outputChunk.arrayOffset() + outputChunk.position(),
                        outputChunk.remaining());
                outputChunk.position(outputChunk.limit());
            }

            outputFile.close();
            outputFile = null;
            return;
        }
    }
    ......
}

v1签名过程很简单,一共分为了三个部分:

  1. 对非目录文件以及过滤文件进行摘要,存储在MANIFEST.MF文件中。
  2. 对MANIFEST.MF文件的进行摘要以及对MANIFEST.MF文件的每个条目内容进行摘要,存储在CERT.SF文件中。
  3. 使用指定的私钥对CERT.SF文件计算签名,然后将签名以及包含公钥信息的数字证书写入 CERT.RSA。 这三个文件也就是我们APK解压后META-INF目录下的文件:


    签名和摘要文件

3.1 .MF文件内容生成

上面已经说了,MANIFEST.MF的内容是通过addDigestsToManifest方法生成的,代码如下:

/**
 * 添加对所有不是目录文件的摘要(SHA1或SHA256)
 */
private static Manifest addDigestsToManifest(JarFile jar, int hashes)
        throws IOException, GeneralSecurityException {
    // 最上面那部分内容
    Manifest input = jar.getManifest();
    Manifest output = new Manifest();
    Attributes main = output.getMainAttributes();
    if (input != null) {
        main.putAll(input.getMainAttributes());
    } else {
        main.putValue("Manifest-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");
    }

    // 根据输入来选择摘要算法
    MessageDigest md_sha1 = null;
    MessageDigest md_sha256 = null;
    if ((hashes & USE_SHA1) != 0) {
        md_sha1 = MessageDigest.getInstance("SHA1");
    }
    if ((hashes & USE_SHA256) != 0) {
        md_sha256 = MessageDigest.getInstance("SHA256");
    }

    byte[] buffer = new byte[4096];
    int num;

    TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
    // 把apk文件所有条目添加到treemap中
    for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
        JarEntry entry = e.nextElement();
        byName.put(entry.getName(), entry);
    }

    // 遍历
    for (JarEntry entry : byName.values()) {
        String name = entry.getName();
        // 如果不是目录并且不是特定的文件 attern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
        //                    Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
        if (!entry.isDirectory() &&
                (stripPattern == null || !stripPattern.matcher(name).matches())) {
            InputStream data = jar.getInputStream(entry);
            while ((num = data.read(buffer)) > 0) {
                if (md_sha1 != null) md_sha1.update(buffer, 0, num);
                if (md_sha256 != null) md_sha256.update(buffer, 0, num);
            }

            Attributes attr = null;
            if (input != null) attr = input.getAttributes(name);
            attr = attr != null ? new Attributes(attr) : new Attributes();
            for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext(); ) {
                Object key = i.next();
                if (!(key instanceof Attributes.Name)) {
                    continue;
                }
                String attributeNameLowerCase =
                        ((Attributes.Name) key).toString().toLowerCase(Locale.US);
                if (attributeNameLowerCase.endsWith("-digest")) {
                    i.remove();
                }
            }
            // 计算摘要 并且使用base64进行encode
            // Add SHA-1 digest if requested
            if (md_sha1 != null) {
                attr.putValue("SHA1-Digest",
                        new String(Base64.encode(md_sha1.digest()), "ASCII"));
            }
            
            if (md_sha256 != null) {
                attr.putValue("SHA-256-Digest",
                        new String(Base64.encode(md_sha256.digest()), "ASCII"));
            }
            output.getEntries().put(name, attr);
        }
    }

    return output;
}

其过程分为三步:

  1. 添加最上面内容信息


    上部分内容
  2. 将APK内容遍历,寻找不为目录并且没有被过滤的文件,对其进行摘要计算。
  3. 将摘要信息写入。
    验证一下:
    AndroidManifest.xml文件对应的摘要
    文件摘要
    这么看这两个值还不相同呢,但是我们仔细看下代码new String(Base64.encode(md_sha1.digest()), "ASCII")这里将摘要内容进行Base64编码后又将其转成String了,我们可以看下:
byte[] bytes = {(byte) 0xD0, (byte) 0xF9, (byte) 0xE4, 0x2B, (byte) 0xB2, (byte) 0xC7, (byte) 0xB0, 0x72, 0x45, (byte) 0x8C, 0x27, (byte) 0xC3, 0x7F, 0x3D, 0x01, 0x78, 0x5C, (byte) 0x82, (byte) 0xA8, (byte) 0xB5};
String ascii = new String(Base64.getEncoder().encode(bytes), "ASCII");
System.out.println(ascii);

输出内容:


输出

简化的代码如下:

    private static Manifest addDigestsToManifest(JarFile jarFile) throws IOException, NoSuchAlgorithmException {
        Manifest input = jarFile.getManifest();
        Manifest output = new Manifest();
        Attributes main = output.getMainAttributes();
        if (input != null) {
            main.putAll(input.getMainAttributes());
        } else {
            main.putValue("Manifest-Version", "1.0");
            main.putValue("Created-By", "1.0 (Android SignApk)");
        }

        MessageDigest sha1 = MessageDigest.getInstance("SHA1");

        TreeMap<String, JarEntry> byName = new TreeMap<>();
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            byName.put(jarEntry.getName(), jarEntry);
        }

        byte[] data = new byte[4096];
        int num = 0;
        for (JarEntry jarEntry : byName.values()) {
            if (!jarEntry.isDirectory()) {
                InputStream inputStream = jarFile.getInputStream(jarEntry);
                while ((num = inputStream.read(data)) > 0) {
                    sha1.update(data, 0, num);
                }

                Attributes attributes = null;
                if (input != null) {
                    attributes = input.getAttributes(jarEntry.getName());
                }

                if (attributes == null) {
                    attributes = new Attributes();
                }

                attributes.putValue("SHA1-Digest", new String(Base64.getEncoder().encode(sha1.digest()), "ASCII"));
                output.getEntries().put(jarEntry.getName(), attributes);
            }
        }

        output.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\MANIFEST.MF"));

        return output;
    }

3.2 .SF文件内容生成

.SF文件内容是需要依赖.MF文件内容:

/**
 * Write a .SF file with a digest of the specified manifest.
 * 写入.sf文件
 */
private static void writeSignatureFile(Manifest manifest, OutputStream out,
                                       int hash, boolean additionallySignedUsingAnApkSignatureScheme)
        throws IOException, GeneralSecurityException {
    Manifest sf = new Manifest();
    Attributes main = sf.getMainAttributes();
    // 添加内容
    main.putValue("Signature-Version", "1.0");
    main.putValue("Created-By", "1.0 (Android SignApk)");
    // v2签名 添加
    if (additionallySignedUsingAnApkSignatureScheme) {
        main.putValue(
                ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
                ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
    }

    MessageDigest md = MessageDigest.getInstance(
            hash == USE_SHA256 ? "SHA256" : "SHA1");
    PrintStream print = new PrintStream(
            new DigestOutputStream(new ByteArrayOutputStream(), md),
            true, "UTF-8");

    manifest.write(print);
    print.flush();
    main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
            new String(Base64.encode(md.digest()), "ASCII"));
    // 这段代码将上面的.MF的内容以
    // Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n
    // 获取其摘要,并且按照相同的格式存储
    Map<String, Attributes> entries = manifest.getEntries();
    for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
        // Digest of the manifest stanza for this entry.
        print.print("Name: " + entry.getKey() + "\r\n");
        for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
            print.print(att.getKey() + ": " + att.getValue() + "\r\n");
        }
        print.print("\r\n");
        print.flush();

        Attributes sfAttr = new Attributes();
        sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
                new String(Base64.encode(md.digest()), "ASCII"));
        sf.getEntries().put(entry.getKey(), sfAttr);
    }

    CountOutputStream cout = new CountOutputStream(out);
    sf.write(cout);

    if ((cout.size() % 1024) == 0) {
        cout.write('\r');
        cout.write('\n');
    }
}

主要两个步骤:

  1. 计算.MF整个文件内容摘要,存放在上面的位置。
  2. 计算.MF每一项内容,将其拼接成Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n格式并计算这段内容的摘要并以相同格式保存
    验证:
String s = "Name: AndroidManifest.xml\r\nSHA1-Digest: 0PnkK7LHsHJFjCfDfz0BeFyCqLU=\r\n\r\n";
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(s.getBytes());
byte[] digest = messageDigest.digest();
System.out.println(new String(Base64.getEncoder().encode(digest), "ASCII"));

输出:


输出
.SF文件内容

简化版:

    private static void signFile(Manifest outPut) throws NoSuchAlgorithmException, IOException {
        Manifest sf = new Manifest();
        Attributes main = sf.getMainAttributes();
        main.putValue("Signature-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");

        MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
        PrintStream print = new PrintStream(
                new DigestOutputStream(new ByteArrayOutputStream(), messageDigest),
                true, "UTF-8");
        outPut.write(print);
        print.flush();

        main.putValue("SHA1-Digest-Manifest",
                new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII"));

        Map<String, Attributes> entries = outPut.getEntries();
        for (Map.Entry<String, Attributes> stringAttributesEntry : entries.entrySet()) {
            print.print("Name: " + stringAttributesEntry.getKey() + "\r\n");
            for (Map.Entry<Object, Object> att : stringAttributesEntry.getValue().entrySet()) {
                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
            }
            print.print("\r\n");
            print.flush();

            Attributes sfAttr = new Attributes();
            sfAttr.putValue("SHA1-Digest",
                    new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII"));
            sf.getEntries().put(stringAttributesEntry.getKey(), sfAttr);
        }


        sf.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\CERT.SF"));
    }

这里有个问题,.SF文件在老的APK(可能是使用v1签名?)中确实是由上面代码生成,但是我看最新的APK文件中.SF文件内容和.MF文件内容一致,猜想可能是v1和v2签名的原因,具体不详。

3.3 签名

签名代码

上面我们得到了.SF文件的内容,通过私钥和公钥就可以对其获得签名信息,根据签名信息即可生成.RSA文件(没有验证过程)。

4.1 v2签名

先复制一下v1签名的漏洞: 1、安卓在4.4中引入了新的执行虚拟机ART,这个虚拟机经过重新的设计,实现了大量的优化,提高了应用的运行效率。与“Janus”有关的一个技术点是,ART允许运行一个raw dex,也就是一个纯粹的dex文件,不需要在外面包装一层zip。而ART的前任DALVIK虚拟机就要求dex必须包装在一个zip内部且名字是classes.dex才能运行。当然ART也支持运行包装在ZIP内部的dex文件,要区别文件是ZIP还是dex,就通过文件头的magic字段进行判断:ZIP文件的开头是‘PK’, 而dex文件的开头是’dex’.

2、ZIP文件的读取方式是通过在文件末尾定位central directory, 然后通过里面的索引定位到各个zip entry,每个entry解压之后都对应一个文件。

通过漏洞就可以知道系统在解压ZIP文件时根据其末尾来解压,但是执行的过程又根据其头部来执行,这样就可以通过注入新的dex在头部来实现攻击的目的。

v2签名官方文档,以下内容来自官方文档:

APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。 使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在APK 签名方案 v2 分块中。


image.png

APK 签名分块

为了保持与当前 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”(位于文件末尾)之前并紧邻该部分。 该分块包含多个“ID-值”对,所采用的封装方式有助于更轻松地在 APK 中找到该分块。APK 的 v2 签名会存储为一个“ID-值”对,其中 ID 为 0x7109871a。

APK 签名方案 v2 分块

APK 由一个或多个签名者/身份签名,每个签名者/身份均由一个签名密钥来表示。该信息会以“APK 签名方案 v2 分块”的形式存储。对于每个签名者,都会存储以下信息:
(签名算法、摘要、签名)元组。摘要会存储起来,以便将签名验证和 APK 内容完整性检查拆开进行。
表示签名者身份的 X.509 证书链。
采用键值对形式的其他属性。
对于每位签名者,都会使用收到的列表中支持的签名来验证 APK。签名算法未知的签名会被忽略。如果遇到多个支持的签名,则由每个实现来选择使用哪个签名。这样一来,以后便能够以向后兼容的方式引入安全系数更高的签名方法。建议的方法是验证安全系数最高的签名。

v2与v1签名最大的区别就是v2修改了APK文件的内容,将其签名块放到了APK文件中(v2签名验证需要验证APK文件的这部分)。
签名块生成,这里代码用的时ApkSignerV2.java

public static ByteBuffer[] sign(
        ByteBuffer inputApk,
        List<SignerConfig> signerConfigs)
        throws ApkParseException, InvalidKeyException, SignatureException {
    ByteBuffer originalInputApk = inputApk;
    inputApk = originalInputApk.slice();
    inputApk.order(ByteOrder.LITTLE_ENDIAN);

    // 获取EoCD位置以及对ZIP文件的校验
    int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
    ......
    inputApk.clear();
    ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
    ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
    
    byte[] eocdBytes = new byte[inputApk.remaining()];
    inputApk.get(eocdBytes);
    ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
    eocd.order(inputApk.order());
    
    Set<Integer> contentDigestAlgorithms = new HashSet<>();
    for (SignerConfig signerConfig : signerConfigs) {
        for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
            contentDigestAlgorithms.add(
                    getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
        }
    }

    // Compute digests of APK contents.
    Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
    try {
        // 计算内容摘要
        contentDigests =
                computeContentDigests(
                        contentDigestAlgorithms,
                        new ByteBuffer[]{beforeCentralDir, centralDir, eocd});
    } catch (DigestException e) {
        throw new SignatureException("Failed to compute digests of APK", e);
    }

    // 生成签名块
    ByteBuffer apkSigningBlock =
            ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));

    centralDirOffset += apkSigningBlock.remaining();
    eocd.clear();
    ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);

    originalInputApk.position(originalInputApk.limit());

    beforeCentralDir.clear();
    centralDir.clear();
    eocd.clear();

    // Insert APK Signing Block immediately before the ZIP Central Directory.
    // 将内容重组
    // 1. ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
    // 2. APK 签名分块
    // 3. ZIP 中央目录
    // 4. ZIP 中央目录结尾
    return new ByteBuffer[]{
            beforeCentralDir,
            apkSigningBlock,
            centralDir,
            eocd,
    };
}

代码也不多,分为三步:

  1. 计算内容摘要。
  2. 对内容摘要进行签名,并生成签名块。
  3. 将签名块添加到原APK文件内容中。

4.1 计算内容摘要

签名后的各个 APK 部分

第 1、3 和 4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些分块则通过一个或多个签名来保护。

第 1、3 和 4 部分的摘要采用以下计算方式,类似于两级 Merkle 树。 每个部分都会被拆分成多个大小为 1 MB(220 个字节)的连续块。每个部分的最后一个块可能会短一些。每个块的摘要均通过字节 0xa5 的连接、块的长度(采用小端字节序的 uint32 值,以字节数计)和块的内容进行计算。顶级摘要通过字节 0x5a 的连接、块数(采用小端字节序的 uint32 值)以及块的摘要的连接(按照块在 APK 中显示的顺序)进行计算。摘要以分块方式计算,以便通过并行处理来加快计算速度。

签名块生成原理

生成代码如下:

/**
 * 计算内容摘要
 *
 * @param digestAlgorithms 摘要算法
 * @param contents         内容,这里三块
 * @return 摘要Map
 * @throws DigestException
 */
private static Map<Integer, byte[]> computeContentDigests(
        Set<Integer> digestAlgorithms,
        ByteBuffer[] contents) throws DigestException {
    // 计算分成1M大小的数量
    int chunkCount = 0;
    for (ByteBuffer input : contents) {
        chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
    }

    // 设置摘要算法和摘要内容的Map
    final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
    for (int digestAlgorithm : digestAlgorithms) {
        int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
        byte[] concatenationOfChunkCountAndChunkDigests =
                new byte[5 + chunkCount * digestOutputSizeBytes];
        concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
        setUnsignedInt32LittleEngian(
                chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
        digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
    }

    int chunkIndex = 0;
    byte[] chunkContentPrefix = new byte[5];
    chunkContentPrefix[0] = (byte) 0xa5;
    // Optimization opportunity: digests of chunks can be computed in parallel.
    // 遍历内容
    for (ByteBuffer input : contents) {
        while (input.hasRemaining()) {
            // 检查剩下的大小,取其和1M中小的哪个
            int chunkSize =
                    Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
            final ByteBuffer chunk = getByteBuffer(input, chunkSize);
            // 遍历摘要算法
            for (int digestAlgorithm : digestAlgorithms) {
                String jcaAlgorithmName =
                        getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
                MessageDigest md;
                try {
                    md = MessageDigest.getInstance(jcaAlgorithmName);
                } catch (NoSuchAlgorithmException e) {
                    throw new DigestException(
                            jcaAlgorithmName + " MessageDigest not supported", e);
                }
                chunk.clear();
                // 设置chunkContentPrefix为0xa5 + chunk.remaining()
                setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
                md.update(chunkContentPrefix);
                md.update(chunk);
                // 获得刚才保存的摘要算法对应的内容,0xa5+length,剩下全为0
                byte[] concatenationOfChunkCountAndChunkDigests =
                        digestsOfChunks.get(digestAlgorithm);
                // 期望的长度
                int expectedDigestSizeBytes =
                        getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
                // 通过摘要算法后已经修改内容的长度
                // 这里是将concatenationOfChunkCountAndChunkDigests内容更新为最新的摘要
                int actualDigestSizeBytes =
                        md.digest(
                                concatenationOfChunkCountAndChunkDigests,
                                5 + chunkIndex * expectedDigestSizeBytes,
                                expectedDigestSizeBytes);
                if (actualDigestSizeBytes != expectedDigestSizeBytes) {
                    throw new DigestException(
                            "Unexpected output size of " + md.getAlgorithm()
                                    + " digest: " + actualDigestSizeBytes);
                }
            }
            chunkIndex++;
        }
    }

    // 对concatenationOfChunkCountAndChunkDigests也就是我们每一块 0xa5 + chunkCount + (0xa5+length+内容的摘要)* chunkCount
    Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
    for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
        int digestAlgorithm = entry.getKey();
        byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
        String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
        MessageDigest md;
        try {
            md = MessageDigest.getInstance(jcaAlgorithmName);
        } catch (NoSuchAlgorithmException e) {
            throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
        }
        result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
    }
    return result;
}

原理也简单,就是将其他部分内容分成1M大小的块,每个块的摘要均通过字节 0xa5 的连接、块的长度(采用小端字节序的 uint32 值,以字节数计)和块的内容进行摘要计算,将计算的结果放到以0xa5+length开头的数组中,最后将其进行摘要计算。

4.2 签名并且生成签名块

签名时对我们刚刚得到的摘要信息进行签名,签名的过程无非就是通过公钥和私钥进行签名计算,生成对应的签名信息(签名过程省略)。 签名块的生成:

/**
 * 生成签名块
 *
 * @param apkSignatureSchemeV2Block apk签名
 * @return
 */
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
    // FORMAT:
    // uint64:  size (excluding this field)
    // repeated ID-value pairs:
    //     uint64:           size (excluding this field)
    //     uint32:           ID
    //     (size - 4) bytes: value
    // uint64:  size (same as the one above)
    // uint128: magic
    int resultSize =
            8 // size
                    + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
                    + 8 // size
                    + 16 // magic
            ;
    ByteBuffer result = ByteBuffer.allocate(resultSize);
    result.order(ByteOrder.LITTLE_ENDIAN);
    long blockSizeFieldValue = resultSize - 8;
    // size of block,以字节数(不含此字段)计 (uint64)
    result.putLong(blockSizeFieldValue);

    long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
    // size
    result.putLong(pairSizeFieldValue);
    // id 0x7109871a
    result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
    // (size - 4) bytes签名块
    result.put(apkSignatureSchemeV2Block);
    // size of block,以字节数计 - 与第一个字段相同 (uint64)
    result.putLong(blockSizeFieldValue);
    // magic“APK 签名分块 42”(16 个字节)
    result.put(APK_SIGNING_BLOCK_MAGIC);

    return result.array();
}

签名块按照格式:

“APK 签名分块”的格式如下(所有数字字段均采用小端字节序):

size of block,以字节数(不含此字段)计 (uint64)
带 uint64 长度前缀的“ID-值”对序列:
ID (uint32)
value(可变长度:“ID-值”对的长度 - 4 个字节)
size of block,以字节数计 - 与第一个字段相同 (uint64)
magic“APK 签名分块 42”(16 个字节

生成。

4.3 生成签名后APK

生成签名后的APK很简单,我们已经获得了每块的内容:

1. Contents of ZIP entries
2. Central Directory
3. End of Central Directory
APK Signing Block

我们只需要将内容合并即可,合并顺序为:

1. Contents of ZIP entries
2. APK Signing Block
3. Central Directory
4. End of Central Directory

代码为:

// Insert APK Signing Block immediately before the ZIP Central Directory.
// 将内容重组
// 1. ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
// 2. APK 签名分块
// 3. ZIP 中央目录
// 4. ZIP 中央目录结尾
return new ByteBuffer[]{
        beforeCentralDir,
        apkSigningBlock,
        centralDir,
        eocd,
};

5 后记

啰里啰唆说了一大堆,终于将签名过程写完了。在Android APK安装时肯定会有对APK的签名信息验证的过程,这部分如果有时间去看Android APK安装流程时再仔细分析了。