七、recovery ota升级包签名生成/校验

本文详细解析了安卓OTA升级包的签名流程,包括签名规则、签名文件位置及签名与校验的具体实现。从签名算法的选择、公钥与私钥的处理到CMS数字签名的生成,再到升级包的校验过程,全面覆盖了升级包安全性的核心技术。

最近一直有两个疑问 升级包签名的规则和签名文件具体的位置,所以大概看了下签名流程并整理出来

大概理解了下 1.如何签名 2.如何校验

 

一.相关整理

首先要大概知道的两个内容

1.CMS数字签名

参考:https://www.ibm.com/developerworks/cn/java/j-lo-cms-ticketbasesso/

大概理解为

签名阶段:取数据的hash 使用私钥进行加密放入签名部分, 签名部分+数据部分 组合为新的zip

校验阶段:取出数据的hash,使用公钥对签名部分的加密hash进行解密,解密后的hash和数据的hash进行对比

2.zip文件的结构

参考:http://blog.sina.com.cn/s/blog_c496a6310102wje4.html

这里主要理解以下其中的End of central directory record

 

二.生成签名

做升级包的流程还是从ota_from_target_files ,这部分略过,直接从签名开始分析,传入的什么参数

最后的签名命令为:

java -Djava.library.path=out/host/linux-x86/lib64

-jar out/host/linux-x86/framework/signapk.jar

-w device/mediatek/security/releasekey.x509.pem ./device/mediatek/security/releasekey.pk8

update.zip  update_sign.zip 2>&1 | tee sign.log

 

所以签名的流程从sign.jar开始,源码位于build/make/tools/signapk/src/com/android/signapk/SignApk.java

jar文件也是从main方法开始 开始上菜,第一道小炒肉

1.main方法

几句话总结

1.解析传入的参数 根据-w判断是否为签名升级包

2.获取输入和输出

3.获取公钥 转化为x509证书格式 readPublicKey方法

4.定义升级内文件将使用的时间戳  (这个其实是为了签名APK时 有点用处,官方文档中说可以减少patch的生成)

5.获取私钥  readPrivateKey

6.定义签名算法的类型 getDigestAlgorithmForOta

7.将以上参数传入签名升级包的方法signWholeFile

    //从main方法开始
    public static void main(String[] args) {
        if (args.length < 4) usage();
        //输出一波参数信息
        //arg = -w
        //arg = device/mediatek/security/releasekey.x509.pem
        //arg = ./device/mediatek/security/releasekey.pk8
        //arg = update.zip
        //arg = update_sign.zip
        for(String arg : args){
          System.out.println("arg = " + arg);
        }
        // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
        // the standard or Bouncy Castle ones.
        Security.insertProviderAt(new OpenSSLProvider(), 1);
        // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
        // DSA which may still be needed.
        // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
        Security.addProvider(new BouncyCastleProvider());

        boolean signWholeFile = false;
        String providerClass = null;
        int alignment = 4;
        Integer minSdkVersionOverride = null;
        boolean signUsingApkSignatureSchemeV2 = true;
        //1.解析传入的参数 根据-w判断是否为签名升级包
        int argstart = 0;
        while (argstart < args.length && args[argstart].startsWith("-")) {
            if ("-w".equals(args[argstart])) {
                signWholeFile = true;
                ++argstart;
            } else if ("-providerClass".equals(args[argstart])) {
                if (argstart + 1 >= args.length) {
                    usage();
                }
                providerClass = args[++argstart];
                ++argstart;
            } else if ("-a".equals(args[argstart])) {
                alignment = Integer.parseInt(args[++argstart]);
                ++argstart;
            } else if ("--min-sdk-version".equals(args[argstart])) {
                String minSdkVersionString = args[++argstart];
                try {
                    minSdkVersionOverride = Integer.parseInt(minSdkVersionString);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(
                            "--min-sdk-version must be a decimal number: " + minSdkVersionString);
                }
                ++argstart;
            } else if ("--disable-v2".equals(args[argstart])) {
                signUsingApkSignatureSchemeV2 = false;
                ++argstart;
            } else {
                usage();
            }
        }
        //args.length = 5
        //argstart = 1
        System.out.println("args.length = " + args.length);
        System.out.println("argstart = " + argstart);
        //如果去掉开头的-w余数等于2 说明参数不对
        if ((args.length - argstart) % 2 == 1) usage();
        //这里numKeys其实是1
        int numKeys = ((args.length - argstart) / 2) - 1;
        //这里的打印也能看出来,当签名升级包的时候 签名只有一个
        if (signWholeFile && numKeys > 1) {
            System.err.println("Only one key may be used with -w.");
            System.exit(2);
        }

        loadProviderIfNecessary(providerClass);
        //2.获取输入和输出
        //获取输入文件 和输出文件的名称 倒数第一个参数和倒数第二个
        String inputFilename = args[args.length-2];
        String outputFilename = args[args.length-1];

        JarFile inputJar = null;
        //定义文件流输出文件
        FileOutputStream outputFile = null;
        
        try {
            //3.获取公钥文件releasekey.x509.pem
            File firstPublicKeyFile = new File(args[argstart+0]);
            //创建x509证书对象
            X509Certificate[] publicKey = new X509Certificate[numKeys];
            try {
                for (int i = 0; i < numKeys; ++i) {
                    int argNum = argstart + i*2;
                    //将公钥转换为x509证书格式
                    publicKey[i] = readPublicKey(new File(args[argNum]));
                    System.out.println("publicKey" + "[" + i + "]" + publicKey[i]);
                }
            } catch (IllegalArgumentException e) {
                System.err.println(e);
                System.exit(1);
            }

            // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
            // 4.创建了一个时间戳 签名之后 文件内容时间为2009 00:00:00.
            long timestamp = 1230768000000L;
            // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
            // timestamp using the current timezone. We thus adjust the milliseconds since epoch
            // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
            timestamp -= TimeZone.getDefault().getOffset(timestamp);
            //5.获取私钥releasekey.pk8
            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]));
                System.out.println("privateKey" + "[" + i + "]" + privateKey[i]);
            }
            //将输入文件转换为了jar文件
            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.

            outputFile = new FileOutputStream(outputFilename);

            // NOTE: Signing currently recompresses any compressed entries using Deflate (default
            // compression level for OTA update files and maximum compession level for APKs).
            // 如果signWholeFile为true 使用signWholeFile方法
            if (signWholeFile) {
                //6.定义签名算法的类型 
                int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
                //digestAlgorithm = 1
                System.out.println("digestAlgorithm = " + digestAlgorithm);
                // inputJar输入文件,也就是原始的升级包
                // firstPublicKeyFile 公钥文件
                // publicKey[0]x509证书格式
                // privateKey[0] 解析后的PrivateKey私钥
                // digestAlgorithm 签名算法的类型
                // outputFile 输出文件
                signWholeFile(inputJar, firstPublicKeyFile,
                        publicKey[0], privateKey[0], digestAlgorithm,
                        timestamp,
                        outputFile);
            } else {
            //后面的不用看了,签名APK用的
            ......
            ......

 

其中用到几个方法,大致过下

1.1 readPublicKey

    private static X509Certificate readPublicKey(File file)
        throws IOException, GeneralSecurityException {
        FileInputStream input = new FileInputStream(file);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            return (X509Certificate) cf.generateCertificate(input);
        } finally {
            input.close();
        }
    }

1.2 readPrivateKey

    /** Read a PKCS#8 format private key.读取PKCS#8格式的私钥 */
    private static PrivateKey readPrivateKey(File file)
        throws IOException, GeneralSecurityException {
        //创建数据输入流
        DataInputStream input = new DataInputStream(new FileInputStream(file));
        try {
            //一次性把长度都读完
            byte[] bytes = new byte[(int) file.length()];
            input.read(bytes);

            /* Check to see if this is in an EncryptedPrivateKeyInfo structure. 检查这是否在EncryptedPrivateKeyInfo结构中*/
            PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
            if (spec == null) {
                spec = new PKCS8EncodedKeySpec(bytes);
            }

            /*
             * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
             * OID and use that to construct a KeyFactory.
             * 现在,它处于PKCS#8 PrivateKeyInfo结构中。 阅读其算法OID并将其用于构造KeyFactory。
             */
            PrivateKeyInfo pki;
            try (ASN1InputStream bIn =
                    new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
                pki = PrivateKeyInfo.getInstance(bIn.readObject());
            }
            String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
            System.out.println("algOid = " + algOid);
            return KeyFactory.getInstance(algOid).generatePrivate(spec);
        } finally {
            input.close();
        }
    }
    /**
     * Decrypt an encrypted PKCS#8 format private key.
     * 解密加密的PKCS#8格式的私钥。
     * Based on ghstark's post on Aug 6, 2006 at
     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
     *
     * @param encryptedPrivateKey The raw data of the private key 私钥的数据
     * @param keyFile The file containing the private key  私钥文件
     */
    private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
        throws GeneralSecurityException {
        EncryptedPrivateKeyInfo epkInfo;
        try {
            epkInfo = new EncryptedPrivateKeyInfo(en
在安卓系统的 Recovery 中读取 `/data/ota` 下升级包时,需要考虑系统加密的情况。很多 Android 手机采用 FUSE 方案,内部 SD 卡占用的是 userdata 的空间。当系统加密后,解密需要 VOLD 参与,而在 Recovery 模式下,VOLD 并未启动,所以若 OTA 升级包保存在 userdata 或内部存储器中,Recovery 无法直接读取 [^3]。 对于非 A/B 升级(传统的 Recovery 升级),bootloader 通过读取 misc 分区引导进入 recovery,通过读 `/cache/recovery/command` 中的指令来执行 OTA 逻辑 [^4]。在执行 OTA 逻辑前,会调用 `verifyPackage()` 函数对 OTA 包进行校验 [^4]。 以下是可能的解决思路和方法: 1. **确保系统未加密**:如果系统未加密,Recovery 可以直接访问 `/data/ota` 目录下的升级包。 2. **解密机制**:若系统加密,需要在 Recovery 中实现解密逻辑,但这需要深入了解系统的加密机制和 VOLD 的工作原理,实现起来较为复杂。 3. **将升级包移至可访问区域**:可以在正常系统模式下,将升级包从 `/data/ota` 移动到 `/cache` 等在 Recovery 模式下可直接访问的区域,然后在 Recovery 中读取该区域的升级包。 示例代码: ```java import android.os.RecoverySystem; import java.io.File; public class OTAUpdate { public static void startUpdate() { try { // 将升级包移至 /cache 目录 File sourceFile = new File("/data/ota/update.zip"); File destFile = new File("/cache/update.zip"); // 实现文件移动逻辑 // ... // 调用 RecoverySystem 进行升级 RecoverySystem.installPackage(context, destFile); } catch (Exception e) { e.printStackTrace(); } } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值