开发者社区> ksuu> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

也谈Android签名机制

简介: 1. 前言 关于Android的签名机制,在一个月前就看过了,当时还写了下流程,感觉没有太大的技术含量就没有记录。最近在看APK安装过程,突然又想起安装过程包含了APK的验证,关于APK的验证无非就是签名的逆过程。
+关注继续查看

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签名。

img_3b58623e45132444000f458058107d4b.png
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目录下的文件:


    img_c582ec7784c3419041df841aa2d9cdf5.png
    签名和摘要文件

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. 添加最上面内容信息


    img_e15aec1542ec2907ea70f8815d0f1508.png
    上部分内容
  2. 将APK内容遍历,寻找不为目录并且没有被过滤的文件,对其进行摘要计算。
  3. 将摘要信息写入。
    验证一下:
    img_cb7416652869924af7de8b75814febce.png
    AndroidManifest.xml文件对应的摘要

    img_3519f81efc60fdc562005e7e463e7baa.png
    文件摘要

    这么看这两个值还不相同呢,但是我们仔细看下代码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);

输出内容:


img_57ed3d1c943c57bbeaf92279f631dd59.png
输出

简化的代码如下:

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

输出:


img_1ef90501e1017850ba2b80946ee1ecae.png
输出

img_1fd0a24346a337bd730ba301a169da8a.png
.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 签名

img_72533ca2e1f42b5fd7b9c4a16a827534.png
签名代码

上面我们得到了.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 分块中。


img_3b58623e45132444000f458058107d4b.png
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 计算内容摘要

img_35cee8151fc43902c7e9d82238630d1b.png
签名后的各个 APK 部分

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

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

img_55658faa6b37353baa2ab03c5938e774.png
签名块生成原理

生成代码如下:

/**
 * 计算内容摘要
 *
 * @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安装流程时再仔细分析了。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Android开发之入口Activity
原文:Android开发之入口Activity Android开发之入口Activity Adnroid App是如何确定入口Activity的? 难道就因为class的类名叫MainActivity,布局文件叫activity_main.xml? 如果这样认为,就大错特错了。
942 0
Android开发之浮动Activity
场景 在使用App时,曾经看到这样一个场景,如下图所示,点击顶部菜单按钮,有一个类似的对话框的列表显示出来,让用户选择其中的一个快递选项,然后选中的快递信息就会填充到底部的Activity中。
674 0
+关注
23
文章
1
问答
文章排行榜
最热
最新