APK签名原理

APK签名原理网上已有多篇分析签名的类似文章,但是都有一个共同的问题,就是概念混乱,混乱的一塌糊涂。在了解APK签名原理之前,首先澄清几个概念:消息摘要-MessageDigest简称摘要,请看英文翻译,是摘要,不是签名,网上几乎所有android签名分析的文章都对这两个概念乱用摘要的链接http://en.wikipedia.org/wiki/Message_digest简

大家好,又见面了,我是你们的朋友全栈君。

网上已有多篇分析签名的类似文章,但是都有一个共同的问题,就是概念混乱,混乱的一塌糊涂。

在了解APK签名原理之前,首先澄清几个概念:

消息摘要 -Message Digest

简称摘要,请看英文翻译,是摘要,不是签名,网上几乎所有APK签名分析的文章都混淆了这两个概念。

摘要的链接http://en.wikipedia.org/wiki/Message_digest

简单的说消息摘要就是在消息数据上,执行一个单向的Hash函数,生成一个固定长度的Hash值,这个Hash值即是消息摘要也称为数字指纹:

消息摘要有以下特点:

1. 通过摘要无法推算得出消息本身

2. 如果修改了消息,那么摘要一定会变化(实际上,由于长明文生成短摘要的Hash必然会产生碰撞),所以这句话并不准确,我们可以改为:很难找到一种模式,修改了消息,而它的摘要不会变化。

消息摘要的这种特性,很适合来验证数据的完整性,比如在网络传输过程中下载一个大文件BigFile,我们会同时从网络下载BigFile和BigFile.md5,BigFile.md5保存BigFile的摘要,我们在本地生成BigFile的消息摘要,和BigFile.md5比较,如果内容相同,则表示下载过程正确。

注意,消息摘要只能保证消息的完整性,并不能保证消息的不可篡改性。

MD5/SHA-0 SHA-1

这些都是摘要生成算法,和签名没有半毛钱关系。如果非要说他们和签名有关系,那就是签名是要借助于摘要技术。

数字签名 – Signature

数字签名百度百科对数字签名有非常清楚的介绍。我这里再罗嗦一下,不懂的去看百度百科。

数字签名就是信息的发送者用自己的私钥对消息摘要加密产生一个字符串,加密算法确保别人无法伪造生成这段字符串,这段数字串也是对信息的发送者发送信息真实性的一个有效证明。

数字签名是 非对称密钥加密技术 + 数字摘要技术 的结合。

数字签名技术是将信息摘要用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的信息摘要,然后接收者用相同的Hash函数对收到的原文产生一个信息摘要,与解密的信息摘要做比对。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改;不同则说明信息被修改过,因此数字签名能保证信息的完整性。并且由于只有发送者才有加密摘要的私钥,所以我们可以确定信息一定是发送者发送的。

数字证书 – Certificate

数字证书是一个经证书授权 中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件。CERT.RSA包含了一个数字签名以及一个数字证书。

需要注意的是Android APK中的CERT.RSA证书是自签名的,并不需要这个证书是第三方权威机构发布或者认证的,用户可以在本地机器自行生成这个自签名证书。

APK签名过程分析

摘要和签名的概念清楚后,我们就可以分析APK 签名过程了。Android提供了APK的签名工具signapk ,使用方法如下:

signapk [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar

publickey.x509.pem包含证书和证书链,证书和证书链中包含了公钥和加密算法;privatekey.pk8是私钥;input.jar是需要签名的jar;output.jar是签名结果

signapk的实现在android/build/tools/signapk/SignApk.java中,主函数main实现如下

    public static void main(String[] args) {
        if (args.length != 4 && args.length != 5) {
            System.err.println("Usage: signapk [-w] " +
                    "publickey.x509[.pem] privatekey.pk8 " +
                    "input.jar output.jar");
            System.exit(2);
        }

        sBouncyCastleProvider = new BouncyCastleProvider();
        Security.addProvider(sBouncyCastleProvider);

        boolean signWholeFile = false;
        int argstart = 0;
        if (args[0].equals("-w")) {
            signWholeFile = true;
            argstart = 1;
        }

        JarFile inputJar = null;
        JarOutputStream outputJar = null;
        FileOutputStream outputFile = null;

        try {
            File publicKeyFile = new File(args[argstart+0]);
            X509Certificate publicKey = readPublicKey(publicKeyFile);

            // Assume the certificate is valid for at least an hour.
            long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;

            PrivateKey privateKey = readPrivateKey(new File(args[argstart+1]));
            inputJar = new JarFile(new File(args[argstart+2]), false);  // Don't verify.

            OutputStream outputStream = null;
            if (signWholeFile) {
                outputStream = new ByteArrayOutputStream();
            } else {
                outputStream = outputFile = new FileOutputStream(args[argstart+3]);
            }
            outputJar = new JarOutputStream(outputStream);

            // For signing .apks, use the maximum compression to make
            // them as small as possible (since they live forever on
            // the system partition).  For OTA packages, use the
            // default compression level, which is much much faster
            // and produces output that is only a tiny bit larger
            // (~0.1% on full OTA packages I tested).
            if (!signWholeFile) {
                outputJar.setLevel(9);
            }

            JarEntry je;

            Manifest manifest = addDigestsToManifest(inputJar);

            // Everything else
            copyFiles(manifest, inputJar, outputJar, timestamp);

            // otacert
            if (signWholeFile) {
                addOtacert(outputJar, publicKeyFile, timestamp, manifest);
            }

            // MANIFEST.MF
            je = new JarEntry(JarFile.MANIFEST_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            manifest.write(outputJar);

            // CERT.SF
            je = new JarEntry(CERT_SF_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSignatureFile(manifest, baos);
            byte[] signedData = baos.toByteArray();
            outputJar.write(signedData);

            // CERT.RSA
            je = new JarEntry(CERT_RSA_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            writeSignatureBlock(new CMSProcessableByteArray(signedData),
                                publicKey, privateKey, outputJar);

            outputJar.close();
            outputJar = null;
            outputStream.flush();

            if (signWholeFile) {
                outputFile = new FileOutputStream(args[argstart+3]);
                signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
                                    outputFile, publicKey, privateKey);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        } finally {
            try {
                if (inputJar != null) inputJar.close();
                if (outputFile != null) outputFile.close();
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    }

生成MAINFEST.MF文件

Manifest manifest = addDigestsToManifest(inputJar);

遍历inputJar中的每一个文件,利用SHA1算法生成这些文件的信息摘要。

            // MANIFEST.MF
            je = new JarEntry(JarFile.MANIFEST_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            manifest.write(outputJar);

生成MAINFEST.MF文件,这个文件包含了input jar包内所有文件内容的摘要值。注意,不会生成下面三个文件的摘要值MANIFEST.MF CERT.SF和CERT.RSA

生成CERT.SF

            // CERT.SF
            je = new JarEntry(CERT_SF_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSignatureFile(manifest, baos);
            byte[] signedData = baos.toByteArray();
            outputJar.write(signedData);

虽然writeSignatureFile字面上看起来是写签名文件,但是CERT.SF的生成和私钥没有一分钱的关系,实际上也不应该有一分钱的关系,这个文件自然不保存任何签名内容。

CERT.SF中保存的是MANIFEST.MF的摘要值,以及MANIFEST.MF中每一个摘要项的摘要值。恕我愚顿,没搞清楚为什么要引入CERT.SF,实际上我觉得签名完全可以用MANIFEST.MF生成。

signedData就是CERT.SF的内容,这个信息摘要在制作签名的时候会用到。

生成CERT.RSA

这个文件保存了签名和公钥证书。签名的生成一定会有私钥参与,签名用到的信息摘要就是CERT.SF内容。

            // CERT.RSA
            je = new JarEntry(CERT_RSA_NAME);
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            writeSignatureBlock(new CMSProcessableByteArray(signedData),
                                publicKey, privateKey, outputJar);

signedData这个数据会作为签名用到的摘要,writeSignatureBlock函数用privateKey对signedData加密生成签名,然后把签名和公钥证书一起保存到CERT.RSA中、

    /** Sign data and write the digital signature to 'out'. */
    private static void writeSignatureBlock(
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
        OutputStream out)
        throws IOException,
               CertificateEncodingException,
               OperatorCreationException,
               CMSException {
        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
        certList.add(publicKey);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
            .setProvider(sBouncyCastleProvider)
            .build(privateKey);
        gen.addSignerInfoGenerator(
            new JcaSignerInfoGeneratorBuilder(
                new JcaDigestCalculatorProviderBuilder()
                .setProvider(sBouncyCastleProvider)
                .build())
            .setDirectSignature(true)
            .build(sha1Signer, publicKey));
        gen.addCertificates(certs);
        CMSSignedData sigData = gen.generate(data, false);

        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
        DEROutputStream dos = new DEROutputStream(out);
        dos.writeObject(asn1.readObject());
    }

翻译下这个函数的注释:对参数data进行签名,然后把生成的数字签名写入参数out中

@data是生成签名的摘要

@publicKey; 是签名用到的私钥对应的证书

@privateKey: 是签名时用到的私钥

@out: 输出文件,也就是CERT.RSA

最终保存在CERT.RSA中的是CERT.SF的数字签名,签名使用privateKey生成的,签名算法会在publicKey中定义。同时还会把publicKey存放在CERT.RSA中,也就是说CERT.RSA包含了签名和签名用到的证书。并且要求这个证书是自签名的。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/131190.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 高通骁龙处理器天梯排行榜2021 高通骁龙处理器发布时间排行

    高通骁龙处理器天梯排行榜2021 高通骁龙处理器发布时间排行第一名:骁龙8881、工艺:搭载最新一代5nm制作工艺,为用户带来最强的处理器性能,5nm的制作工艺,带来最为顶尖的技术、成本、功能性能要求。我用的手机就是活动时7.5折抢购的点击开抢http://shouji.adiannao.cn/72、核心:使用了超大核+大核+小核的三丛集架构,其中超大核为CortexX1,大核为CortexA78,小核为CortexA55。3、体验:超级大核Cortex-X1拥有1MB的L2缓存,A78大核L2缓存则为256KB,可以给你更好的性能体验,用户带来

    2022年5月23日
    223
  • springboot框架图解_spring boot框架搭建

    springboot框架图解_spring boot框架搭建本文链接:https://blog.csdn.net/qq_41063141/article/details/83239941

    2022年8月20日
    6
  • navicat永久激活码最新【2021免费激活】

    (navicat永久激活码最新)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~https://javaforall.net/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~0X1Z…

    2022年3月28日
    116
  • 理解dropout

    理解dropout开篇明义,dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。dropout是CNN中防止过拟合提高效果的一个大杀器,但对于其为何有效,却众说纷纭。在下读到两篇代表性的论文,代表两种不同的观点,特此分享给大家。

    2022年4月27日
    44
  • 错误:Unbound classpath container: ‘JRE System Library [JavaSE-1.7]’ in project[通俗易懂]

    错误:Unbound classpath container: ‘JRE System Library [JavaSE-1.7]’ in project[通俗易懂]用新的Eclipse创建Maven项目时出现的问题。经过查找资料,是jre问题。解决方案:              项目右键—&gt;Properties—&gt;Java Build Path—&gt;Libraries—&gt;按照下图操作 再重新添加Jre可以选择工作空间默认的jre,也可以重新添加外部的jre,如果选择默认的点击完成…

    2022年6月13日
    37
  • matlab画图标签,Matlab绘图[通俗易懂]

    matlab画图标签,Matlab绘图[通俗易懂]要使用plot函数来绘制图形,需要执行以下步骤:通过指定要绘制函数的变量x的值的范围来定义x。定义函数,y=f(x)调用plot命令,如下:plot(x,y)以下示例将演示该概念。下面绘制x的值范围是从0到100,使用简单函数y=x,增量值为5。创建脚本文件并键入以下代码-x=[0:5:100];y=x;plot(x,y)执行上面示例代码,得到以下结果-下面再来一个例子来绘制…

    2022年6月24日
    34

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号