Android中的簽名和簽名文件的生成過(guò)程

概述

這個(gè)玩意簡(jiǎn)單說(shuō)起來(lái)很簡(jiǎn)單,詳細(xì)描述起來(lái)很復(fù)雜,復(fù)雜在什么地方呢,首先有一塊陌生的知識(shí)點(diǎn),包括但不限于證書(shū),數(shù)字簽名,加密,密鑰,keytool,keystore,md5,sha1,sha256,base64編碼,文件hash等等。這些東西不是屬于哪門(mén)語(yǔ)言,或者屬于哪個(gè)平臺(tái)。

還是從Key Store和keytool說(shuō)起吧

我們?cè)诎l(fā)布一款app的時(shí)候,肯定要對(duì)這款app進(jìn)行簽名,那么如何生成一個(gè)簽過(guò)名的apk呢?現(xiàn)在AndroidStudio越來(lái)越強(qiáng)大了,直接選擇 Build-->Generate Singed APK 然后選擇next 就可以。

在選擇的過(guò)程中,他會(huì)讓你選擇Key Store Path 。那么問(wèn)題來(lái)了這個(gè)keystore什么東西呢?如果硬要翻譯的話,叫做key(密鑰)store(倉(cāng)庫(kù))。顧名思義里面的存放的是密鑰,這個(gè)密鑰呢分為公鑰和私鑰。如果要類(lèi)比的話,我感覺(jué)類(lèi)似于sqlite文件的一張表,里面存放了格式化的數(shù)據(jù)。每一條都是一個(gè)公私鑰對(duì)和一些相關(guān)信息??梢杂泻芏鄺l。

要想弄明白一個(gè)東西是啥,是如何運(yùn)行的,最好的辦法就是弄一個(gè)出來(lái)觀察一下,為了便于觀察,我借助AndroidStudio來(lái)創(chuàng)建這個(gè)文件:


image.png
  1. 一開(kāi)始的時(shí)候,我們假設(shè)沒(méi)有這個(gè)keystore,然后我們選中Create New...
  2. 為keystore文件選擇一個(gè)存儲(chǔ)位置,我放到了桌面上
  3. 給這個(gè)文件添加打開(kāi)密碼(類(lèi)似于壓縮文件的加密),這里我設(shè)置的密碼都是123456
  4. 在 keystore 中添加一個(gè)條目,這個(gè)條目命名為 my_key_1 ,當(dāng)然名字隨便取,這個(gè)條目里面存放的是,公私鑰對(duì),和下面6中的一些證書(shū)的基本信息。
  5. 在 keystore 中的每個(gè)條目都有自己?jiǎn)为?dú)的密碼保護(hù)。
  6. 證書(shū)有效期
  7. 證書(shū)輸入一些關(guān)于您自己的信息。此信息不會(huì)顯示在應(yīng)用中,但會(huì)作為 APK 的一部分包含在您的證書(shū)中。

點(diǎn)擊確定之后,這個(gè)條目(key)就被保存到keystore中了。當(dāng)然這keystore里面可以創(chuàng)建多個(gè)條目(key)。

keytool是jdk提供的一個(gè)把鑰匙和證書(shū)儲(chǔ)存到keystore中的工具。而默認(rèn)的keystore就是一個(gè)文件,它用一個(gè)密碼保護(hù),要想通過(guò)keytool命令打開(kāi)keystore文件,必須輸入密碼。所有的keystore中條目入口(鑰匙和信任書(shū)入口)是通過(guò)唯一的別名,并且也需要密碼保護(hù)。別名是不區(qū)分大小寫(xiě)的。通過(guò)keytool可以很方便的操作keystore 文件。一篇文章

下面列幾個(gè)常用的命令,首先進(jìn)入到存放keystore 文件的目錄下。下面my_key.keystore是文件名。

  1. 查看證書(shū)
    $ keytool -list -v -keystore my_key.keystore -storepass 123456
  2. 從 keystore 導(dǎo)出證書(shū)
    $ keytool -export -alias my_key_1 -file my_new.crt -keystore my_key.keystore
    其中 my_key_1 是別名,my_new.crt 是要導(dǎo)出的到的文件的名字,my_key.keystore是 keystore 的路徑

數(shù)字簽名和數(shù)字證書(shū)

公鑰和私鑰的兩種作用:公共鑰匙用來(lái)加密數(shù)據(jù),私有鑰匙用來(lái)計(jì)算簽名;公鑰加密的消息只能用私鑰解密,私鑰簽名的消息只能用公鑰檢驗(yàn)簽名。

數(shù)據(jù)摘要:主要有MD5,SHA-1等,用直白的話來(lái)說(shuō)就是通過(guò)算法,對(duì)輸入的消息(一般是一些二進(jìn)制數(shù)據(jù))運(yùn)算后得到一個(gè)固定長(zhǎng)度的輸出,一般來(lái)說(shuō)輸入不同,得到的摘要也是不同的,并且沒(méi)有辦法通過(guò)摘要還原數(shù)據(jù)。(其實(shí)這也很好理解,顧名思義,好比我們語(yǔ)文中對(duì)一篇文章做摘要,不同的文章做的一般不同,而且沒(méi)法通過(guò)摘要得到整篇文章的原文)
數(shù)字簽名: 數(shù)字簽名是非對(duì)稱密鑰加密技術(shù)數(shù)字摘要技術(shù)的應(yīng)用。(一句話總結(jié)一下就是,私鑰加密后的消息摘要就是數(shù)字簽名)
數(shù)字簽名應(yīng)用場(chǎng)景 :“發(fā)送報(bào)文時(shí),發(fā)送方用一個(gè)哈希函數(shù)從報(bào)文文本中生成報(bào)文摘要,然后用自己的私人密鑰對(duì)這個(gè)摘要進(jìn)行加密,這個(gè)加密后的摘要將作為報(bào)文的數(shù)字簽名和報(bào)文一起發(fā)送給接收方,接收方首先用與發(fā)送方一樣的哈希函數(shù)從接收到的原始報(bào)文中計(jì)算出報(bào)文摘要,接著再用發(fā)送方的公用密鑰來(lái)對(duì)報(bào)文附加的數(shù)字簽名進(jìn)行解密,如果這兩個(gè)摘要相同、那么接收方就能確認(rèn)該數(shù)字簽名是發(fā)送方的。

數(shù)字簽名有兩種功效:一是能確定消息確實(shí)是由發(fā)送方簽名并發(fā)出來(lái)的,因?yàn)閯e人假冒不了發(fā)送方的簽名。二是數(shù)字簽名能確定消息的完整性。因?yàn)閿?shù)字簽名的特點(diǎn)是它代表了文件的特征,文件如果發(fā)生改變,數(shù)字摘要的值也將發(fā)生變化。不同的文件將得到不同的數(shù)字摘要。 一次數(shù)字簽名涉及到一個(gè)哈希函數(shù)、發(fā)送者的公鑰、發(fā)送者的私鑰。”

但是在【數(shù)字簽名的應(yīng)用場(chǎng)景】里面描述的有個(gè)問(wèn)題,就是前提是接收消息的一方,拿到的公鑰必須是正確的。如果公鑰都被篡改了,那么后面的一切都錯(cuò)了。數(shù)字證書(shū)可以保證數(shù)字證書(shū)里面的公鑰確實(shí)是這個(gè)證書(shū)所有者的,這樣就可以解決公鑰的安全發(fā)放了。那么這個(gè)證書(shū)是哪里來(lái)的呢。這就用到了一種約定。我們理論上認(rèn)為某些有公信力的機(jī)構(gòu)發(fā)放的證書(shū)是安全的。我們把這些發(fā)放證書(shū)的機(jī)構(gòu)叫做CA(Certificate Authority)。CA用自己的私鑰對(duì)申請(qǐng)證書(shū)的人的公鑰和一些基本信息,做簽名,然后把申請(qǐng)者的公鑰,基本信息,和數(shù)字簽名放到一起組成一個(gè)證書(shū)。而CA本身也會(huì)生成一個(gè)證書(shū),是自簽名的,也叫根證書(shū),會(huì)內(nèi)置在操作系統(tǒng)里面。

apk的簽名過(guò)程

先看一下一個(gè)可安裝的apk包是怎么搞出來(lái)的


Google官方的一個(gè)apk編譯打包的流程圖.png

從上圖可以看到,簽名發(fā)生在打包過(guò)程中的倒數(shù)第二步,而且簽名針對(duì)的是已經(jīng)存在的apk包,并不會(huì)影響我們寫(xiě)的代碼。事實(shí)也確實(shí)是如此,Android的簽名,大致的簽名原理就是對(duì)未簽名的apk里面的所有文件計(jì)算hash,然后保存起來(lái)(MANIFEST.MF),然后在對(duì)這些hash計(jì)算hash保存起來(lái)(CERT.SF),然后在計(jì)算hash,然后再通過(guò)我們上面生成的keystore里面的私鑰進(jìn)行加密并保存(CERT.RSA)。很抽象是吧,我們對(duì)照著打包好的apk,來(lái)看一下

其實(shí)對(duì)我來(lái)說(shuō),最常用到簽名的時(shí)候,并不是在開(kāi)發(fā)一個(gè)app的時(shí)候,而是在逆向一個(gè)app的時(shí)候,因?yàn)锳ndroid系統(tǒng)不允許安裝未簽名的apk,所以我們反編譯了別人的apk,想要安裝的話,必須重新簽名。Android反編譯后重新打包apk

來(lái)創(chuàng)建一個(gè)測(cè)試用的as工程,全部用默認(rèn)的就行。只不過(guò)要修改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'
            //簽名配置,如果把下面注釋掉,打出的包是未簽名的,如果放開(kāi)注釋那么是簽名的包
            //signingConfig signingConfigs.config
        }
    }
    productFlavors {
    }
}
//不關(guān)心依賴

我們可以先生成一個(gè)未簽名的apk,然后在生成一個(gè)簽名的apk。

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

然后進(jìn)入當(dāng)前項(xiàng)目目錄下面運(yùn)行(aR 是簡(jiǎn)寫(xiě),assembleRelease ):

$ ./gradlew aR 

然后未簽名的apk會(huì)存放在 MyApplication/app/build/outputs/apk/release/app-release-unsigned.apk 里面。

  1. 如何生成未簽名的apk
    就是把 signingConfig 那行注釋放開(kāi)就行了其余的一樣,然后未簽名的apk會(huì)存放在 MyApplication/app/build/outputs/apk/release/app-release.apk 里面。

  2. 簽名apk和未簽名的apk的差別在哪

看文件大小,差不多的大。我們用解壓軟件打開(kāi)看一下發(fā)現(xiàn)簽名后的/META-INF文件夾里面多了兩個(gè)文件,而且MANIFEST.MF 的大小和內(nèi)容是不同的 如下圖:

簽名和未簽名的apk目錄結(jié)構(gòu)對(duì)比.png

  1. 如何給未簽名的apk 簽名 命令行運(yùn)行
$ 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里面的一個(gè)alias

注:
jarsigner 是JDK提供的針對(duì)jar包簽名的通用工具,
位于JDK/bin/jarsigner.exe
apksigner 是Google官方提供的針對(duì)Android apk簽名及驗(yàn)證的專用工具,
位于Android SDK/build-tools/sdk版本/apksigner.bat (最終調(diào)用Android SDK/build-tools/sdk版本/lib/apksigner.jar)
不管是apk包,還是jar包,本質(zhì)都是zip格式的壓縮包,所以它們的簽名過(guò)程都差不多(僅限V1簽名),
以上兩個(gè)工具都可以對(duì)Android apk包進(jìn)行簽名.

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

首先來(lái)看一下這三個(gè)文件里面都保存了什么?

注:這幾個(gè)文件的生成過(guò)程見(jiàn)Android源碼里面的SignApk.java ,一篇文章,這篇文章里面是Android2.2的源碼里面的。但是每個(gè)Android版本的sdk中,在 SDK/build-tools/sdk版本/lib/apksigner.jar下也會(huì)有個(gè)簽名工具的jar包。也可以通過(guò) jd-gui 工具反編譯每個(gè)sdk版本下面的 apksigner.jar 查看來(lái)分析簽名中以下三個(gè)文件的生成過(guò)程。

1. MANIFEST.MF

首先要明白,這個(gè)玩意不是Android搞出來(lái),這個(gè)在java打成jar包的時(shí)候,就會(huì)存在這么個(gè)清單文件。而且這個(gè)文件的格式是固定的,java里面有個(gè)類(lèi)能夠解析這個(gè)文件:java.util.jar.Manifest; 然后把這個(gè)文件拖到sublime里面打開(kāi)(把我們的apk的后綴名改成zip,然后用解壓軟件解壓):

MANIFEST.MF文件部分內(nèi)容截圖.png

manifest 文件的格式: 是很簡(jiǎn)單的,每一行都是 名-值 對(duì)應(yīng)的:屬性名開(kāi)頭,接著是 ":" ,然后是屬性值,每行最多72個(gè)字符,如果需要增加,你可以在下一行續(xù)行,續(xù)行以空格開(kāi)頭,以空格開(kāi)頭的行都會(huì)被視為前一行的續(xù)行,所有在開(kāi)頭的屬性都是全局的。

manifest 文件的內(nèi)容:
這里面內(nèi)容的含義是啥?其中Name 對(duì)應(yīng)的是apk包里面的所有文件的文件名,而SHA-256-Digest 是指的是這個(gè)文件的求的sha的值的Base64編碼。為了驗(yàn)證我們拿解壓出來(lái)的 AndroidManifest.xml做個(gè)試驗(yàn)。
有兩步要做:

  1. 計(jì)算文件的hash的值
    怎么求這個(gè)文件的hash值呢。有兩種方式:自己造輪子,自己用Python,或者java或者shell,寫(xiě)個(gè)計(jì)算文件hash值的工具類(lèi);另一種方式可以借助工具。這里我借助工具來(lái)看,這里有個(gè)在線網(wǎng)站 計(jì)算文件的hash的值,當(dāng)然除了這個(gè)還有很多別的工具只要能計(jì)算文件hash就行。然后把我們的文件 AndroidManifest.xml上傳上去得到如下結(jié)果:
MD5 Hash    833f7812f9f9c4c7d4de99aa39866515
SHA1 Hash   d5bb8c084310a5993832bdeeec70ab917088aa8a
SHA256 Hash c3328e24fccba47c73adaaa13a1f9180d2388b42b2899169d628158ce4810c1e

這里計(jì)算出來(lái)的是hash的 16進(jìn)制。觀察我們的MANIFEST.MF文件里面的兩行,里面的計(jì)算hash的一行發(fā)現(xiàn),用的是 sha 256 算法,并且是Base64編碼的,所以需要轉(zhuǎn)換:

Name: AndroidManifest.xml
SHA-256-Digest: wzKOJPzLpHxzraqhOh+RgNI4i0KyiZFp1igVjOSBDB4=
  1. 把計(jì)算出來(lái)的文件的hash的16進(jìn)制的值轉(zhuǎn)成Base64的編碼
    注意Base64 僅僅是種編碼方式而已。
    現(xiàn)在要把c3328e24fccba47c73adaaa13a1f9180d2388b42b2899169d628158ce4810c1e這個(gè)16進(jìn)制轉(zhuǎn)換成Base64,方式同上也有兩種:自己寫(xiě)代碼,借助工具。百度搜索16 進(jìn)制轉(zhuǎn)base64 第一條就是個(gè)工具網(wǎng)站。轉(zhuǎn)換結(jié)果截圖如下:
    16進(jìn)制轉(zhuǎn)Base64.png

好到這里,就知道MANIFEST.MF 這個(gè)文件里面每一組內(nèi)容的來(lái)源了??梢砸来悟?yàn)證剩下的所有的文件的hash。

2. CERT.SF

這個(gè)文件其實(shí)也是一個(gè)Manifest文件格式的文件,同樣可以拖到sublime里面看一下。

屏幕快照 2018-04-12 18.53.11.png

其實(shí)對(duì)比上面 MANIFEST.MF 里面的每一組數(shù)據(jù),發(fā)現(xiàn),Name行是一一對(duì)應(yīng)的。只不過(guò)SHA-256-Digest行不同而已,那么這一行又是怎么算出來(lái)的。這里看前面給出的SignApk.java 的源碼。里面又這個(gè)文件的生成過(guò)程(包含了三個(gè)回車(chē)換行符"\r\n")。就是把MANIFEST.MF 里面的每一組計(jì)算hash。但是這個(gè)每一組有點(diǎn)特殊。在源碼里面大約在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();
       //構(gòu)建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();
        //開(kāi)始循環(huán)讀入MANIFEST.MF頭一下的每一組數(shù)據(jù),計(jì)算hash,并寫(xiě)入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');
        }
    }

注:關(guān)于回車(chē)換行符,換行符‘\n’和回車(chē)符‘\r’。Windows系統(tǒng)里,文件每行結(jié)尾是"<回車(chē)><換行>""\r\n";Unix系統(tǒng)里,文件每行結(jié)尾是"<換行>",即'\n'。

從上面源碼中可以看到,我們要計(jì)算Name: AndroidManifest.xml這個(gè)條目在CERT.SF中對(duì)應(yīng)的條目的hash,必須要計(jì)算下面這些字符串的hash,其中包含了三個(gè)回車(chē)換行(注意是回車(chē)換行):

image.png

為什么在這里不斷的重復(fù)這個(gè)回車(chē)換行呢。就是我們本來(lái)有個(gè)很簡(jiǎn)單的方法,如果想要計(jì)算一段字符串的hash,可以把這段字符串copy到文件中,然后用錢(qián)買(mǎi)的工具去計(jì)算這個(gè)文件的hash,但是在Mac系統(tǒng)上出現(xiàn)一個(gè)致命的錯(cuò)誤:就是在copy過(guò)程中回車(chē)換行符 \r\n中的 回車(chē)符\r (ASCII 碼16進(jìn)制為0D)丟了。這里我們先不深究這個(gè)回車(chē)換行的問(wèn)題了。

同樣要計(jì)算這段字符串的hash也是兩種方式。既然我們借助工具可能存在問(wèn)題,那么我們模仿SignApk.java 生成CERT.SF 的過(guò)程自己寫(xiě)一段驗(yàn)證代碼。這段代碼是Java的,不屬于Android,為了簡(jiǎn)單我省去了異常檢測(cè):

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進(jìn)制編碼:" + bigInteger.toString(16));
    }
}

打印結(jié)果

生成的摘要Base64編碼:vsFKOu9AFKSRe1RzLHH3zTHrdLRo+ysSX8Lv1YKRaiM=
生成的摘要16進(jìn)制編碼:bec14a3aef4014a4917b54732c71f7cd31eb74b468fb2b125fc2efd582916a23

當(dāng)然我們也可以模仿SignApk.java的生成過(guò)程,對(duì) MANIFEST.MF 里面的所有條目分析一遍,源碼在文末給出。

3. CERT.RSA

和簽名相關(guān)的apk里面的文件總共有三個(gè),目前分析完了兩個(gè)了,還有一個(gè),這個(gè)有點(diǎn)麻煩,我們把它也拖到sublime里面打開(kāi)看一下,發(fā)現(xiàn)全是二進(jìn)制。

屏幕快照 CERT.RSA.png

我們分別來(lái)對(duì)比 android-2.2.2_r1 和 android-6.0.0_r5源碼里面的SignApk.java 的源碼來(lái)觀察這個(gè)文件的生成過(guò)程,來(lái)確定這個(gè)文件到底是個(gè)啥?

android-2.2.2_r1 SignApk 源碼.png
android-6.0.0_r5 SignApk 源碼.png

其實(shí)前面的兩個(gè)截圖中完成的功能是相同的就是生成了CERT.RSA文件。只不過(guò)生成方式不同,在Android2.x版本的時(shí)候是用的java的sun.security.pkcs.*包里面的工具,生成的。但是在4.x之后開(kāi)始使用了一個(gè)開(kāi)源庫(kù):官網(wǎng)文檔 。上面的PKCS7的功能比較清晰簡(jiǎn)單,就是是封裝了 PKCS7 格式的 CERT.RSA 文件里面該有的內(nèi)容??梢钥吹竭@個(gè)文件從上往下總共包含四塊內(nèi)容:

CERT.RSA 文件內(nèi)容分塊圖.png

前面兩塊我們不用太關(guān)心,主要看最后兩塊,一個(gè)是1到多張公鑰證書(shū),一個(gè)是n條加密后的消息摘要。

下面來(lái)看一下每一個(gè)X509公鑰證書(shū)包含哪些內(nèi)容(圖片來(lái)自互聯(lián)網(wǎng),稍作修改):

X509公鑰證書(shū)文件內(nèi)容.png

證書(shū)就是普通的我們常見(jiàn)的公鑰證書(shū)的二進(jìn)制格式而已,所以沒(méi)有什么好討論的,不過(guò)是從keystore里面導(dǎo)出的而已,可以自己驗(yàn)證一下,通過(guò)命令行工具從keystore里面導(dǎo)出公鑰證書(shū),然后拿到16進(jìn)制字節(jié)碼從CERT.RSA里面查找是不是能找到相應(yīng)的片段。

但是最后一個(gè)SingerInfo對(duì)象表示的是啥?去看這個(gè)類(lèi)的源碼,里面有個(gè)很重要的方法getEncryptedDigest 這里面存放的是真正的文件簽名,那么是哪個(gè)文件的簽名,是 CERT.SF 的。他在源碼里面用了個(gè) FilterOutputStream的子類(lèi)對(duì)象,在生成 CERT.SF 的時(shí)候,把寫(xiě)的文件流字節(jié)數(shù)組也進(jìn)行了簽名。然后保存到SingerInfo 對(duì)象里面,最后一塊寫(xiě)入到CERT.RSA文件里面。我們反向操作,把這個(gè)簽名的數(shù)組給搞出來(lái):

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);
        //通過(guò)java給提供的 PKCS7 工具類(lèi),解析CERT.RSA文件
        PKCS7 pkcs7 = new PKCS7(fis);
        //然后提取AlgorithmId
        AlgorithmId algorithmId = pkcs7.getDigestAlgorithmIds()[0];
        //然后提取 X509Certificate 所有的證書(shū),但是我們只有一張證書(shū)
        //這里為了簡(jiǎn)單只去第一個(gè)就行了,每一個(gè)X509Certificate 代表一個(gè)證書(shū)
        X509Certificate certificate = pkcs7.getCertificates()[0];
        //簽名者信息,最主要的是這個(gè)類(lèi)里面的文件簽名,同樣只取一個(gè)
        SignerInfo signerInfo = pkcs7.getSignerInfos()[0];
        
        System.out.println("algorithmId 16進(jìn)制打印:" + new BigInteger(1, algorithmId.encode()).toString(16));
//        System.out.println("ContentInfo:" + new BigInteger(1, contentInfo.getContent().getData()).toString(16));
        //編碼后的證書(shū),下面的命令行可以導(dǎo)出
        //$ openssl pkcs7 -inform DER -in CERT.RSA -print_certs | openssl x509 -outform DER -out CERT.cer
        System.out.println("第一張公鑰證書(shū)的16進(jìn)制打印:" + new BigInteger(1, certificate.getEncoded()).toString(16));
        // 對(duì)CERT.SF 求摘要,然后加密后的東西放到這個(gè)里面
        System.out.println("signerInfo 中保存的文件CERT.SF的加密簽名 16進(jìn)制打印:" + new BigInteger(1, signerInfo.getEncryptedDigest()).toString(16));


        //我要要獲取的簽名其實(shí)就是對(duì)整個(gè)證書(shū) 也就是certificate.getEncoded() 的摘要 md5,或者sha1
        //其實(shí)從 keystore 里面導(dǎo)出證書(shū),然后求md5和這個(gè)一樣
        MessageDigest messageDigest = null;
        messageDigest = MessageDigest.getInstance("md5");
        // 計(jì)算md5函數(shù)
        messageDigest.update(certificate.getEncoded());
        // digest()最后確定返回md5 hash值,返回值為8為字符串。因?yàn)閙d5 hash值是16位的hex值,實(shí)際上就是8位的字符
        // BigInteger函數(shù)則將8位的字符串轉(zhuǎn)換成16位hex值,用字符串來(lái)表示;得到字符串形式的hash值
        String hex = new BigInteger(1, messageDigest.digest()).toString(16);
        System.out.println("獲取apk簽名:" + hex);
    }
}

最終打印的結(jié)果

algorithmId 16進(jìn)制打印:300d06096086480165030402010500
第一張公鑰證書(shū)的16進(jìn)制打印:3082034d30820235a003020102020467617791300d06092a864886f70d01010b05003057310b30090603550406130230303110300e060355040813074265694a696e673110300e060355040713074265694a696e67310b3009060355040a13024141310b3009060355040b13024141310a30080603550403130151301e170d3138303332323038333430375a170d3433303331363038333430375a3057310b30090603550406130230303110300e060355040813074265694a696e673110300e060355040713074265694a696e67310b3009060355040a13024141310b3009060355040b13024141310a3008060355040313015130820122300d06092a864886f70d01010105000382010f003082010a02820101008f8dbba6345e60e716c9816507b06733ef4e6d94fa48e3075239fcfbfd60fd982176ff5583c877d9627f5bd047622fc80adffb91398fdb3b317825a845222ca580fdbb7f2a673d8f45340ef573cea72743c35a5fd94b9f23eac3079c1613f604e21317039b97956c9476f877cb3706c95c76c05bd1544fcdd337b000e007e31567c944f7f9b807029f4f68ebcc02db97d9e867bb0b31b2223a34cc27f2a57ef86db8b7160e283a5f1b33381e8edda7cae19c27efb8bfe75168a3abe4cfd83e45ef92c108be584753b5ffd90f5b686bd77303734e06dc4a827bed8cfca9a42ff41bbc703a9e2307313b2a6592cbe6226320bb7c8c4b481fe2b48448460fc0a59b0203010001a321301f301d0603551d0e041604148ab20e29227404298469eed528135bccf6c51551300d06092a864886f70d01010b05000382010100357c2094b085bdadaa7ac811c45765880f17496a75076514147a4ad8b9c8dc056f83cf81cda1b2b8904c7b79786e85aa1055abf56981dbadef549cd56b0d4a79d9280d3a39e00eac0614aef6b073beda7b13fd322b9a6d3a82ee987fcd166bb987dc2a173a2ac64a05b1c5b232a621ba45107517692bd7581236d5e21715ba45bc37b005fe8ca7edc124814cbc337736ca0851ead276c320f3034531fc8b479adb2aac2eb1836d6c9d4f3281a178ab3744a1f950db4fb6bd2572bcdf4f48deafdd31081d8c15c2bae4dc58040f643182f80d2455b690a909c074e7e58faae15f02020dc676508fd3727c611618ae0c590e2893afeaf30b75e318c3a0b1c99167
signerInfo 中保存的文件CERT.SF的加密簽名 16進(jìn)制打印:33ab13d6cb6dbe350d28db217de7173bdbeb046095cd8e6b29bd34526350c56bb2ed4bb7fd528feac61b8c6da70eb688697b0be040a33a7377a2a179649f2aba1298e6ed0168243e122c6fb960d6c6186b1fb79166a70a1de9d224d48564a11e13c43db465521a59beab86c0f53c5e0dd2b1b86b1da92560acfa7842d4973481d4aa1f1efb15ad14e624b936738d8b44845202de6b97f4a40567b3657cefd8f03c25d75f00ab33b1d960ac5b38c26586c343fead3250a9fbfc010b01dafa47c3dc588c05885c485cfd8537a9d01c7ffd9606abf2bc7f176886a3128bbd7efbb07b195711e847c69cceb815ee198b0210e0918d0b7323896af0f3b17a3a8e17af
獲取apk簽名:ec85ee04014aa1dff9857e08b4301fc8

然后我們借助一款工具(010Eidtor)來(lái)查看我們打印出來(lái)的這些16進(jìn)制的東西,是不是正確的,工具主要是用來(lái)比對(duì)字節(jié)碼的,沒(méi)有工具也可以用眼看。在010Eidtor 里面搜索我們打印的證書(shū)或者簽名的16進(jìn)制碼。發(fā)現(xiàn)確實(shí)能找到。


010Eidtor 查看CERT.RSA.png

到目前為止,我們成功的通過(guò)java的PKCS7的工具類(lèi),解析一下 apk里面生成的CERT.RSA這個(gè)文件。這個(gè)文件中大體包含了四塊有用的數(shù)據(jù)?,F(xiàn)在來(lái)看就剩下最后塊,簽名信息我們沒(méi)有驗(yàn)證是怎么生成的了,從源碼來(lái)看,最后這一塊是把CERT.SF 這個(gè)文件,通過(guò)keystore里面的私鑰來(lái)計(jì)算簽名得到的。

兩個(gè)概念(前面有了,再寫(xiě)一遍):

消息摘要:它是一個(gè)唯一對(duì)應(yīng)一個(gè)消息或文本的固定長(zhǎng)度的值,它由一個(gè)單向Hash加密函數(shù)對(duì)消息進(jìn)行作用而產(chǎn)生。如果消息在途中改變了,則接收者通過(guò)對(duì)收到消息的新產(chǎn)生的摘要與原摘要比較,就可知道消息是否被改變了。因此消息摘要保證了消息的完整性。消息摘要采用單向Hash 函數(shù)將需加密的明文"摘要"成一串密文,這一串密文亦稱為數(shù)字指紋(Finger Print)。它有固定的長(zhǎng)度,且不同的明文摘要成密文,其結(jié)果總是不同的,而同樣的明文其摘要必定一致。這樣這串摘要便可成為驗(yàn)證明文是否是"真身"的"指紋"了。

數(shù)字簽名:數(shù)字簽名算法可以看做是一種帶有密鑰的消息摘要算法,并且這種密鑰包含了公鑰和私鑰。也就是說(shuō),數(shù)字簽名算法是非對(duì)稱加密算法和消息摘要算法的結(jié)合體。

驗(yàn)證代碼如下:

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:  驗(yàn)證簽名生成過(guò)程中,計(jì)算 CERT.SF 文件的簽名信息并寫(xiě)入到
 * CERT.RSA 文件的末尾
 */
public class SignFile {
    public static void main(String[] args) throws Exception {

        //讀取keystore文件到KeyStore對(duì)象
        FileInputStream in = new FileInputStream("file/my_key.keystore");
        KeyStore ks = KeyStore.getInstance("JKS");// JKS: Java KeyStoreJKS,可以有多種類(lèi)型
        //文件輸入流,keystore的密碼
        ks.load(in, "123456".toCharArray());
        in.close();

        //從keystore中讀取證書(shū)和私鑰
        String alias = "my_key_1";  // 記錄的別名
        String pswd = "123456";   // 記錄的訪問(wèn)密碼

        //然后從keystore 里面的一條里面獲取私鑰
        KeyPair keyPair = getKeyPair(ks, alias, pswd.toCharArray());

        //獲取 CERT.SF 文件的內(nèi)存實(shí)例,這樣讀取導(dǎo)致條目順序不對(duì)
        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
        //下面即將打印我們自己計(jì)算的文件的值
        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;
    }
}

通過(guò)最后打印出來(lái)的結(jié)果,和上面通過(guò)解析CERT.RSA 文件得到的結(jié)果進(jìn)行比對(duì)??梢园l(fā)現(xiàn)確實(shí)是這么生成的。對(duì)于CERT.RSA 文件的驗(yàn)證過(guò)程全部是用的是sun.security.*包里面的工具類(lèi)來(lái)解析的。由于google后來(lái)廢棄了這些,用了新的開(kāi)源庫(kù)。所以我們可以用新的開(kāi)源庫(kù)。解析過(guò)程大同小異。

所有代碼下載地址:https://download.csdn.net/download/niyingxunzong/10355331

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 關(guān)于作者: 李濤,騰訊Android工程師,14年加入騰訊SNG增值產(chǎn)品部,期間主要負(fù)責(zé)手Q動(dòng)漫、企鵝電競(jìng)等項(xiàng)目的...
    稻草人_3e17閱讀 3,893評(píng)論 0 10
  • 轉(zhuǎn)載: http://blog.csdn.net/kickxxx/article/details/18252881...
    笑羋閱讀 1,547評(píng)論 0 1
  • 對(duì)于 Android 開(kāi)發(fā)者而言, APK 簽名的重要性不言而喻。Android 7.0 后 APK 簽名已經(jīng)從基...
    Cavabiao閱讀 10,097評(píng)論 7 30
  • 對(duì)于 Android 開(kāi)發(fā)者而言, APK 簽名的重要性不言而喻。Android 7.0 后 APK 簽名已經(jīng)從基...
    稻草人_3e17閱讀 700評(píng)論 0 4
  • 1月10日,星期三,晴 與女兒談?wù)勎夜ぷ鞯哪切┦?今天是一年一度的110宣傳日,我?guī)缀鯊脑缑Φ酵?,等下午接女兒時(shí)已...
    月兒貝貝閱讀 260評(píng)論 0 0

友情鏈接更多精彩內(nèi)容