Android中的签名和签名文件的生成过程

概述

这个玩意简单说起来很简单,详细描述起来很复杂,复杂在什么地方呢,首先有一块陌生的知识点,包括但不限于证书,数字签名,加密,密钥,keytool,keystore,md5,sha1,sha256,base64编码,文件hash等等。这些东西不是属于哪门语言,或者属于哪个平台。

还是从Key Store和keytool说起吧

我们在发布一款app的时候,肯定要对这款app进行签名,那么如何生成一个签过名的apk呢?现在AndroidStudio越来越强大了,直接选择 Build–>Generate Singed APK 然后选择next 就可以。

在选择的过程中,他会让你选择Key Store Path 。那么问题来了这个keystore什么东西呢?如果硬要翻译的话,叫做key(密钥)store(仓库)。顾名思义里面的存放的是密钥,这个密钥呢分为公钥和私钥。如果要类比的话,我感觉类似于sqlite文件的一张表,里面存放了格式化的数据。每一条都是一个公私钥对和一些相关信息。可以有很多条。

要想弄明白一个东西是啥,是如何运行的,最好的办法就是弄一个出来观察一下,为了便于观察,我借助AndroidStudio来创建这个文件:

《Android中的签名和签名文件的生成过程》 image.png

  1. 一开始的时候,我们假设没有这个keystore,然后我们选中Create New…
  2. 为keystore文件选择一个存储位置,我放到了桌面上
  3. 给这个文件添加打开密码(类似于压缩文件的加密),这里我设置的密码都是123456
  4. 在 keystore 中添加一个条目,这个条目命名为 my_key_1 ,当然名字随便取,这个条目里面存放的是,公私钥对,和下面6中的一些证书的基本信息。
  5. 在 keystore 中的每个条目都有自己单独的密码保护。
  6. 证书有效期
  7. 证书输入一些关于您自己的信息。此信息不会显示在应用中,但会作为 APK 的一部分包含在您的证书中。

点击确定之后,这个条目(key)就被保存到keystore中了。当然这keystore里面可以创建多个条目(key)。

keytool是jdk提供的一个把钥匙和证书储存到keystore中的工具。而默认的keystore就是一个文件,它用一个密码保护,要想通过keytool命令打开keystore文件,必须输入密码。所有的keystore中条目入口(钥匙和信任书入口)是通过唯一的别名,并且也需要密码保护。别名是不区分大小写的。通过keytool可以很方便的操作keystore 文件。一篇文章

下面列几个常用的命令,首先进入到存放keystore 文件的目录下。下面my_key.keystore是文件名。

  1. 查看证书
    $ keytool -list -v -keystore my_key.keystore -storepass 123456
  2. 从 keystore 导出证书
    $ keytool -export -alias my_key_1 -file my_new.crt -keystore my_key.keystore
    其中 my_key_1 是别名,my_new.crt 是要导出的到的文件的名字,my_key.keystore是 keystore 的路径

数字签名和数字证书

公钥和私钥的两种作用:公共钥匙用来加密数据,私有钥匙用来计算签名;公钥加密的消息只能用私钥解密,私钥签名的消息只能用公钥检验签名。

数据摘要:主要有MD5,SHA-1等,用直白的话来说就是通过算法,对输入的消息(一般是一些二进制数据)运算后得到一个固定长度的输出,一般来说输入不同,得到的摘要也是不同的,并且没有办法通过摘要还原数据。(其实这也很好理解,顾名思义,好比我们语文中对一篇文章做摘要,不同的文章做的一般不同,而且没法通过摘要得到整篇文章的原文)
数字签名: 数字签名是非对称密钥加密技术数字摘要技术的应用。(一句话总结一下就是,私钥加密后的消息摘要就是数字签名)
数字签名应用场景 :“发送报文时,发送方用一个哈希函数从报文文本中生成报文摘要,然后用自己的私人密钥对这个摘要进行加密,这个加密后的摘要将作为报文的数字签名和报文一起发送给接收方,接收方首先用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要,接着再用发送方的公用密钥来对报文附加的数字签名进行解密,如果这两个摘要相同、那么接收方就能确认该数字签名是发送方的。

数字签名有两种功效:一是能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名。二是数字签名能确定消息的完整性。因为数字签名的特点是它代表了文件的特征,文件如果发生改变,数字摘要的值也将发生变化。不同的文件将得到不同的数字摘要。 一次数字签名涉及到一个哈希函数、发送者的公钥、发送者的私钥。”

但是在【数字签名的应用场景】里面描述的有个问题,就是前提是接收消息的一方,拿到的公钥必须是正确的。如果公钥都被篡改了,那么后面的一切都错了。数字证书可以保证数字证书里面的公钥确实是这个证书所有者的,这样就可以解决公钥的安全发放了。那么这个证书是哪里来的呢。这就用到了一种约定。我们理论上认为某些有公信力的机构发放的证书是安全的。我们把这些发放证书的机构叫做CA(Certificate Authority)。CA用自己的私钥对申请证书的人的公钥和一些基本信息,做签名,然后把申请者的公钥,基本信息,和数字签名放到一起组成一个证书。而CA本身也会生成一个证书,是自签名的,也叫根证书,会内置在操作系统里面。

apk的签名过程

先看一下一个可安装的apk包是怎么搞出来的

《Android中的签名和签名文件的生成过程》 Google官方的一个apk编译打包的流程图.png

从上图可以看到,签名发生在打包过程中的倒数第二步,而且签名针对的是已经存在的apk包,并不会影响我们写的代码。事实也确实是如此,Android的签名,大致的签名原理就是对未签名的apk里面的所有文件计算hash,然后保存起来(MANIFEST.MF),然后在对这些hash计算hash保存起来(CERT.SF),然后在计算hash,然后再通过我们上面生成的keystore里面的私钥进行加密并保存(CERT.RSA)。很抽象是吧,我们对照着打包好的apk,来看一下

其实对我来说,最常用到签名的时候,并不是在开发一个app的时候,而是在逆向一个app的时候,因为Android系统不允许安装未签名的apk,所以我们反编译了别人的apk,想要安装的话,必须重新签名。Android反编译后重新打包apk

来创建一个测试用的as工程,全部用默认的就行。只不过要修改gradle文件如下:

apply plugin: 'com.android.application'

android {
    signingConfigs {
        config {
            keyAlias 'my_key_1'
            keyPassword '123456'
            storeFile file('/Users/liuqiang/Desktop/my_key.keystore')
            storePassword '123456'
        }
    }
    compileSdkVersion 26
    defaultConfig {
        applicationId "me.febsky.myapplication"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //签名配置,如果把下面注释掉,打出的包是未签名的,如果放开注释那么是签名的包
            //signingConfig signingConfigs.config
        }
    }
    productFlavors {
    }
}
//不关心依赖

我们可以先生成一个未签名的apk,然后在生成一个签名的apk。

  1. 如何生成未签名的apk
    在app/build.gradle 里面把添加签名的配置注释掉:
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //签名配置
            //signingConfig signingConfigs.config
        }
    }

然后进入当前项目目录下面运行(aR 是简写,assembleRelease ):

$ ./gradlew aR 

然后未签名的apk会存放在 MyApplication/app/build/outputs/apk/release/app-release-unsigned.apk 里面。

  1. 如何生成未签名的apk
    就是把 signingConfig 那行注释放开就行了其余的一样,然后未签名的apk会存放在 MyApplication/app/build/outputs/apk/release/app-release.apk 里面。

  2. 签名apk和未签名的apk的差别在哪

看文件大小,差不多的大。我们用解压软件打开看一下发现签名后的/META-INF文件夹里面多了两个文件,而且MANIFEST.MF 的大小和内容是不同的 如下图:

《Android中的签名和签名文件的生成过程》 签名和未签名的apk目录结构对比.png

  1. 如何给未签名的apk 签名 命令行运行
$ jarsigner -verbose -keystore my_key.keystore -signedjar result_singed.apk app-release-unsigned.apk my_key_1
  • my_key.keystore keystore文件的路径
  • app-release-unsigned.apk 输入的未签名文件路径
  • my_key_1 keystore里面的一个alias

注:
jarsigner 是JDK提供的针对jar包签名的通用工具,
位于JDK/bin/jarsigner.exe
apksigner 是Google官方提供的针对Android apk签名及验证的专用工具,
位于Android SDK/build-tools/sdk版本/apksigner.bat (最终调用Android SDK/build-tools/sdk版本/lib/apksigner.jar)
不管是apk包,还是jar包,本质都是zip格式的压缩包,所以它们的签名过程都差不多(仅限V1签名),
以上两个工具都可以对Android apk包进行签名.

签名中 MANIFEST.MF CERT.SF和 CERT.RSA是怎么生成的

首先来看一下这三个文件里面都保存了什么?

注:这几个文件的生成过程见Android源码里面的SignApk.java ,一篇文章,这篇文章里面是Android2.2的源码里面的。但是每个Android版本的sdk中,在 SDK/build-tools/sdk版本/lib/apksigner.jar下也会有个签名工具的jar包。也可以通过 jd-gui 工具反编译每个sdk版本下面的 apksigner.jar 查看来分析签名中以下三个文件的生成过程。

1. MANIFEST.MF

首先要明白,这个玩意不是Android搞出来,这个在java打成jar包的时候,就会存在这么个清单文件。而且这个文件的格式是固定的,java里面有个类能够解析这个文件:java.util.jar.Manifest; 然后把这个文件拖到sublime里面打开(把我们的apk的后缀名改成zip,然后用解压软件解压):

《Android中的签名和签名文件的生成过程》 MANIFEST.MF文件部分内容截图.png

manifest 文件的格式: 是很简单的,每一行都是 名-值 对应的:属性名开头,接着是 “:” ,然后是属性值,每行最多72个字符,如果需要增加,你可以在下一行续行,续行以空格开头,以空格开头的行都会被视为前一行的续行,所有在开头的属性都是全局的。

manifest 文件的内容:
这里面内容的含义是啥?其中Name 对应的是apk包里面的所有文件的文件名,而SHA-256-Digest 是指的是这个文件的求的sha的值的Base64编码。为了验证我们拿解压出来的 AndroidManifest.xml做个试验。
有两步要做:

  1. 计算文件的hash的值
    怎么求这个文件的hash值呢。有两种方式:自己造轮子,自己用Python,或者java或者shell,写个计算文件hash值的工具类;另一种方式可以借助工具。这里我借助工具来看,这里有个在线网站 计算文件的hash的值,当然除了这个还有很多别的工具只要能计算文件hash就行。然后把我们的文件 AndroidManifest.xml上传上去得到如下结果:
MD5 Hash    833f7812f9f9c4c7d4de99aa39866515
SHA1 Hash   d5bb8c084310a5993832bdeeec70ab917088aa8a
SHA256 Hash c3328e24fccba47c73adaaa13a1f9180d2388b42b2899169d628158ce4810c1e

这里计算出来的是hash的 16进制。观察我们的MANIFEST.MF文件里面的两行,里面的计算hash的一行发现,用的是 sha 256 算法,并且是Base64编码的,所以需要转换:

Name: AndroidManifest.xml
SHA-256-Digest: wzKOJPzLpHxzraqhOh+RgNI4i0KyiZFp1igVjOSBDB4=
  1. 把计算出来的文件的hash的16进制的值转成Base64的编码
    注意Base64 仅仅是种编码方式而已。
    现在要把c3328e24fccba47c73adaaa13a1f9180d2388b42b2899169d628158ce4810c1e这个16进制转换成Base64,方式同上也有两种:自己写代码,借助工具。百度搜索16 进制转base64 第一条就是个工具网站。转换结果截图如下:
    《Android中的签名和签名文件的生成过程》 16进制转Base64.png

好到这里,就知道MANIFEST.MF 这个文件里面每一组内容的来源了。可以依次验证剩下的所有的文件的hash。

2. CERT.SF

这个文件其实也是一个Manifest文件格式的文件,同样可以拖到sublime里面看一下。

《Android中的签名和签名文件的生成过程》 屏幕快照 2018-04-12 18.53.11.png

其实对比上面 MANIFEST.MF 里面的每一组数据,发现,Name行是一一对应的。只不过SHA-256-Digest行不同而已,那么这一行又是怎么算出来的。这里看前面给出的SignApk.java 的源码。里面又这个文件的生成过程(包含了三个回车换行符”\r\n”)。就是把MANIFEST.MF 里面的每一组计算hash。但是这个每一组有点特殊。在源码里面大约在397行:

 /** Write a .SF file with a digest of the specified manifest. */
    private static void writeSignatureFile(Manifest manifest, OutputStream out,
                                           int hash)
        throws IOException, GeneralSecurityException {
        Manifest sf = new Manifest();
        Attributes main = sf.getMainAttributes();
       //构建CERT.SF 头部的四行
        main.putValue("Signature-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");
        MessageDigest md = MessageDigest.getInstance(
            hash == USE_SHA256 ? "SHA256" : "SHA1");
        PrintStream print = new PrintStream(
            new DigestOutputStream(new ByteArrayOutputStream(), md),
            true, "UTF-8");
        // Digest of the entire manifest
        manifest.write(print);
        print.flush();
        main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
                      new String(Base64.encode(md.digest()), "ASCII"));
        Map<String, Attributes> entries = manifest.getEntries();
        //开始循环读入MANIFEST.MF头一下的每一组数据,计算hash,并写入CERT.SF中
        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-Manifest",
                            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');
        }
    }

注:关于回车换行符,换行符‘\n’和回车符‘\r’。Windows系统里,文件每行结尾是”<回车><换行>””\r\n”;Unix系统里,文件每行结尾是”<换行>”,即’\n’。

从上面源码中可以看到,我们要计算Name: AndroidManifest.xml这个条目在CERT.SF中对应的条目的hash,必须要计算下面这些字符串的hash,其中包含了三个回车换行(注意是回车换行):

《Android中的签名和签名文件的生成过程》 image.png

为什么在这里不断的重复这个回车换行呢。就是我们本来有个很简单的方法,如果想要计算一段字符串的hash,可以把这段字符串copy到文件中,然后用钱买的工具去计算这个文件的hash,但是在Mac系统上出现一个致命的错误:就是在copy过程中回车换行符 \r\n中的 回车符\r (ASCII 码16进制为0D)丢了。这里我们先不深究这个回车换行的问题了。

同样要计算这段字符串的hash也是两种方式。既然我们借助工具可能存在问题,那么我们模仿SignApk.java 生成CERT.SF 的过程自己写一段验证代码。这段代码是Java的,不属于Android,为了简单我省去了异常检测:

public class SHA256 {
    public static void main(String[] args) throws Exception {
        BASE64Encoder base64Encoder = new BASE64Encoder();
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        outputStream.write(("Name: AndroidManifest.xml\r\n").getBytes());
        outputStream.write(("SHA-256-Digest: wzKOJPzLpHxzraqhOh+RgNI4i0KyiZFp1igVjOSBDB4=\r\n").getBytes());
        outputStream.write("\r\n".getBytes());

        byte[] hashSha256 = md.digest(outputStream.toByteArray());
        String sha256Base64Str = base64Encoder.encode(hashSha256);
        BigInteger bigInteger = new BigInteger(1, hashSha256);
        System.out.println("生成的摘要Base64编码:" + sha256Base64Str);
        System.out.println("生成的摘要16进制编码:" + bigInteger.toString(16));
    }
}

打印结果

生成的摘要Base64编码:vsFKOu9AFKSRe1RzLHH3zTHrdLRo+ysSX8Lv1YKRaiM=
生成的摘要16进制编码:bec14a3aef4014a4917b54732c71f7cd31eb74b468fb2b125fc2efd582916a23

当然我们也可以模仿SignApk.java的生成过程,对 MANIFEST.MF 里面的所有条目分析一遍,源码在文末给出。

3. CERT.RSA

和签名相关的apk里面的文件总共有三个,目前分析完了两个了,还有一个,这个有点麻烦,我们把它也拖到sublime里面打开看一下,发现全是二进制。

《Android中的签名和签名文件的生成过程》 屏幕快照 CERT.RSA.png

我们分别来对比 android-2.2.2_r1 和 android-6.0.0_r5源码里面的SignApk.java 的源码来观察这个文件的生成过程,来确定这个文件到底是个啥?

《Android中的签名和签名文件的生成过程》 android-2.2.2_r1 SignApk 源码.png
《Android中的签名和签名文件的生成过程》 android-6.0.0_r5 SignApk 源码.png

其实前面的两个截图中完成的功能是相同的就是生成了CERT.RSA文件。只不过生成方式不同,在Android2.x版本的时候是用的java的sun.security.pkcs.*包里面的工具,生成的。但是在4.x之后开始使用了一个开源库:官网文档 。上面的PKCS7的功能比较清晰简单,就是是封装了 PKCS7 格式的 CERT.RSA 文件里面该有的内容。可以看到这个文件从上往下总共包含四块内容:

《Android中的签名和签名文件的生成过程》 CERT.RSA 文件内容分块图.png

前面两块我们不用太关心,主要看最后两块,一个是1到多张公钥证书,一个是n条加密后的消息摘要。

下面来看一下每一个X509公钥证书包含哪些内容(图片来自互联网,稍作修改):

《Android中的签名和签名文件的生成过程》 X509公钥证书文件内容.png

证书就是普通的我们常见的公钥证书的二进制格式而已,所以没有什么好讨论的,不过是从keystore里面导出的而已,可以自己验证一下,通过命令行工具从keystore里面导出公钥证书,然后拿到16进制字节码从CERT.RSA里面查找是不是能找到相应的片段。

但是最后一个SingerInfo对象表示的是啥?去看这个类的源码,里面有个很重要的方法getEncryptedDigest 这里面存放的是真正的文件签名,那么是哪个文件的签名,是 CERT.SF 的。他在源码里面用了个 FilterOutputStream的子类对象,在生成 CERT.SF 的时候,把写的文件流字节数组也进行了签名。然后保存到SingerInfo 对象里面,最后一块写入到CERT.RSA文件里面。我们反向操作,把这个签名的数组给搞出来:

import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;
import sun.security.x509.AlgorithmId;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class PKCS7Demo {
    public static void main(String[] args) throws CertificateException, IOException, NoSuchAlgorithmException {
        File file = new File("file/META-INF/CERT.RSA");
        FileInputStream fis = new FileInputStream(file);
        //通过java给提供的 PKCS7 工具类,解析CERT.RSA文件
        PKCS7 pkcs7 = new PKCS7(fis);
        //然后提取AlgorithmId
        AlgorithmId algorithmId = pkcs7.getDigestAlgorithmIds()[0];
        //然后提取 X509Certificate 所有的证书,但是我们只有一张证书
        //这里为了简单只去第一个就行了,每一个X509Certificate 代表一个证书
        X509Certificate certificate = pkcs7.getCertificates()[0];
        //签名者信息,最主要的是这个类里面的文件签名,同样只取一个
        SignerInfo signerInfo = pkcs7.getSignerInfos()[0];
        
        System.out.println("algorithmId 16进制打印:" + new BigInteger(1, algorithmId.encode()).toString(16));
//        System.out.println("ContentInfo:" + new BigInteger(1, contentInfo.getContent().getData()).toString(16));
        //编码后的证书,下面的命令行可以导出
        //$ openssl pkcs7 -inform DER -in CERT.RSA -print_certs | openssl x509 -outform DER -out CERT.cer
        System.out.println("第一张公钥证书的16进制打印:" + new BigInteger(1, certificate.getEncoded()).toString(16));
        // 对CERT.SF 求摘要,然后加密后的东西放到这个里面
        System.out.println("signerInfo 中保存的文件CERT.SF的加密签名 16进制打印:" + new BigInteger(1, signerInfo.getEncryptedDigest()).toString(16));


        //我要要获取的签名其实就是对整个证书 也就是certificate.getEncoded() 的摘要 md5,或者sha1
        //其实从 keystore 里面导出证书,然后求md5和这个一样
        MessageDigest messageDigest = null;
        messageDigest = MessageDigest.getInstance("md5");
        // 计算md5函数
        messageDigest.update(certificate.getEncoded());
        // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
        // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
        String hex = new BigInteger(1, messageDigest.digest()).toString(16);
        System.out.println("获取apk签名:" + hex);
    }
}

最终打印的结果

algorithmId 16进制打印:300d06096086480165030402010500
第一张公钥证书的16进制打印:3082034d30820235a003020102020467617791300d06092a864886f70d01010b05003057310b30090603550406130230303110300e060355040813074265694a696e673110300e060355040713074265694a696e67310b3009060355040a13024141310b3009060355040b13024141310a30080603550403130151301e170d3138303332323038333430375a170d3433303331363038333430375a3057310b30090603550406130230303110300e060355040813074265694a696e673110300e060355040713074265694a696e67310b3009060355040a13024141310b3009060355040b13024141310a3008060355040313015130820122300d06092a864886f70d01010105000382010f003082010a02820101008f8dbba6345e60e716c9816507b06733ef4e6d94fa48e3075239fcfbfd60fd982176ff5583c877d9627f5bd047622fc80adffb91398fdb3b317825a845222ca580fdbb7f2a673d8f45340ef573cea72743c35a5fd94b9f23eac3079c1613f604e21317039b97956c9476f877cb3706c95c76c05bd1544fcdd337b000e007e31567c944f7f9b807029f4f68ebcc02db97d9e867bb0b31b2223a34cc27f2a57ef86db8b7160e283a5f1b33381e8edda7cae19c27efb8bfe75168a3abe4cfd83e45ef92c108be584753b5ffd90f5b686bd77303734e06dc4a827bed8cfca9a42ff41bbc703a9e2307313b2a6592cbe6226320bb7c8c4b481fe2b48448460fc0a59b0203010001a321301f301d0603551d0e041604148ab20e29227404298469eed528135bccf6c51551300d06092a864886f70d01010b05000382010100357c2094b085bdadaa7ac811c45765880f17496a75076514147a4ad8b9c8dc056f83cf81cda1b2b8904c7b79786e85aa1055abf56981dbadef549cd56b0d4a79d9280d3a39e00eac0614aef6b073beda7b13fd322b9a6d3a82ee987fcd166bb987dc2a173a2ac64a05b1c5b232a621ba45107517692bd7581236d5e21715ba45bc37b005fe8ca7edc124814cbc337736ca0851ead276c320f3034531fc8b479adb2aac2eb1836d6c9d4f3281a178ab3744a1f950db4fb6bd2572bcdf4f48deafdd31081d8c15c2bae4dc58040f643182f80d2455b690a909c074e7e58faae15f02020dc676508fd3727c611618ae0c590e2893afeaf30b75e318c3a0b1c99167
signerInfo 中保存的文件CERT.SF的加密签名 16进制打印:33ab13d6cb6dbe350d28db217de7173bdbeb046095cd8e6b29bd34526350c56bb2ed4bb7fd528feac61b8c6da70eb688697b0be040a33a7377a2a179649f2aba1298e6ed0168243e122c6fb960d6c6186b1fb79166a70a1de9d224d48564a11e13c43db465521a59beab86c0f53c5e0dd2b1b86b1da92560acfa7842d4973481d4aa1f1efb15ad14e624b936738d8b44845202de6b97f4a40567b3657cefd8f03c25d75f00ab33b1d960ac5b38c26586c343fead3250a9fbfc010b01dafa47c3dc588c05885c485cfd8537a9d01c7ffd9606abf2bc7f176886a3128bbd7efbb07b195711e847c69cceb815ee198b0210e0918d0b7323896af0f3b17a3a8e17af
获取apk签名:ec85ee04014aa1dff9857e08b4301fc8

然后我们借助一款工具(010Eidtor)来查看我们打印出来的这些16进制的东西,是不是正确的,工具主要是用来比对字节码的,没有工具也可以用眼看。在010Eidtor 里面搜索我们打印的证书或者签名的16进制码。发现确实能找到。

《Android中的签名和签名文件的生成过程》 010Eidtor 查看CERT.RSA.png

到目前为止,我们成功的通过java的PKCS7的工具类,解析一下 apk里面生成的CERT.RSA这个文件。这个文件中大体包含了四块有用的数据。现在来看就剩下最后块,签名信息我们没有验证是怎么生成的了,从源码来看,最后这一块是把CERT.SF 这个文件,通过keystore里面的私钥来计算签名得到的。

两个概念(前面有了,再写一遍):

消息摘要:它是一个唯一对应一个消息或文本的固定长度的值,它由一个单向Hash加密函数对消息进行作用而产生。如果消息在途中改变了,则接收者通过对收到消息的新产生的摘要与原摘要比较,就可知道消息是否被改变了。因此消息摘要保证了消息的完整性。消息摘要采用单向Hash 函数将需加密的明文”摘要”成一串密文,这一串密文亦称为数字指纹(Finger Print)。它有固定的长度,且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。这样这串摘要便可成为验证明文是否是”真身”的”指纹”了。

数字签名:数字签名算法可以看做是一种带有密钥的消息摘要算法,并且这种密钥包含了公钥和私钥。也就是说,数字签名算法是非对称加密算法和消息摘要算法的结合体。

验证代码如下:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.*;

/**
 * Author: liuqiang
 * Date: 2018-04-17
 * Time: 14:30
 * Description:  验证签名生成过程中,计算 CERT.SF 文件的签名信息并写入到
 * CERT.RSA 文件的末尾
 */
public class SignFile {
    public static void main(String[] args) throws Exception {

        //读取keystore文件到KeyStore对象
        FileInputStream in = new FileInputStream("file/my_key.keystore");
        KeyStore ks = KeyStore.getInstance("JKS");// JKS: Java KeyStoreJKS,可以有多种类型
        //文件输入流,keystore的密码
        ks.load(in, "123456".toCharArray());
        in.close();

        //从keystore中读取证书和私钥
        String alias = "my_key_1";  // 记录的别名
        String pswd = "123456";   // 记录的访问密码

        //然后从keystore 里面的一条里面获取私钥
        KeyPair keyPair = getKeyPair(ks, alias, pswd.toCharArray());

        //获取 CERT.SF 文件的内存实例,这样读取导致条目顺序不对
        File file = new File("file/META-INF/CERT.SF");
        FileInputStream fis = new FileInputStream(file);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        byte[] buf = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buf)) > 0) {
            outputStream.write(buf, 0, bytesRead);
        }

        //用私钥签名文件
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(keyPair.getPrivate());

        signature.update(outputStream.toByteArray(), 0, outputStream.size());
        byte[] bb = signature.sign();

        //从 CERT.RSA 文件里面获取的值 33ab13d6cb6dbe350d28db217de7173bdbeb046095cd8e6b29bd34526350c56bb2ed4bb7fd528feac61b8c6da70eb688697b0be040a33a7377a2a179649f2aba1298e6ed0168243e122c6fb960d6c6186b1fb79166a70a1de9d224d48564a11e13c43db465521a59beab86c0f53c5e0dd2b1b86b1da92560acfa7842d4973481d4aa1f1efb15ad14e624b936738d8b44845202de6b97f4a40567b3657cefd8f03c25d75f00ab33b1d960ac5b38c26586c343fead3250a9fbfc010b01dafa47c3dc588c05885c485cfd8537a9d01c7ffd9606abf2bc7f176886a3128bbd7efbb07b195711e847c69cceb815ee198b0210e0918d0b7323896af0f3b17a3a8e17af
        //下面即将打印我们自己计算的文件的值
        System.out.println(new BigInteger(1, bb).toString(16));
    }


    //得到KeyPair
    public static KeyPair getKeyPair(KeyStore keystore, String alias, char[] password) {
        try {
            Key key = keystore.getKey(alias, password);
            if (key instanceof PrivateKey) {
                java.security.cert.Certificate cert = keystore.getCertificate(alias);
                PublicKey publicKey = cert.getPublicKey();
                return new KeyPair(publicKey, (PrivateKey) key);
            }
        } catch (UnrecoverableKeyException e) {
        } catch (NoSuchAlgorithmException e) {
        } catch (KeyStoreException e) {
        }
        return null;
    }
}

通过最后打印出来的结果,和上面通过解析CERT.RSA 文件得到的结果进行比对。可以发现确实是这么生成的。对于CERT.RSA 文件的验证过程全部是用的是sun.security.*包里面的工具类来解析的。由于google后来废弃了这些,用了新的开源库。所以我们可以用新的开源库。解析过程大同小异。

所有代码下载地址:https://download.csdn.net/download/niyingxunzong/10355331

    原文作者:千山万水迷了鹿
    原文地址: https://www.jianshu.com/p/d22eb5576c25
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞