APK ROM 簽名原理

本文原創作者:Cloud Chou. 歡迎轉載,請註明出處和本文鏈接

前言

這些天有人問我關於APK或者ROM簽名的原理,因爲先前接觸過簽名的東西,就想當然地認爲在META-INF下存在3個文件, 一個是清單文件MANIFEST.MF,一個是簽名後的CERT.SF,一個是公鑰文件CERT.RSA,網上不少資料也是這樣的觀點。後來查看了簽名工具的源代碼才發現大錯特錯,CERT.SF根本不是用私鑰對MANIFSET.MF簽名後的文件,只是對MANIFEST.MF的每個條目再次計算摘要後的文件。現在想想凡事不可輕易斷言,還是實事求是才能找到真理。接下來將根據源碼詳細分析APK或者ROM簽名的原理。

什麼是簽名

首先我們得知道什麼是摘要,摘要是指採用單向Hash函數對數據進行計算生成的固定長度的Hash值,摘要算法有Md5,Sha1等,Md5生成的Hash值是128位的數字,即16個字節,用十六進制表示是32個字符,Sha1生成的Hash值是160位的數字,即20個字節,用十六進制表示是40個字符。我們是不能通過摘要推算出用於計算摘要的數據,如果修改了數據,那麼它的摘要一定會變化(其實這句話並不正確,只是很難正好找到不同的數據,而他們的摘要值正好相等)。摘要經常用於驗證數據的完整性,很多下載網站都會列出下載文件的md5值或者sha1值。

摘要和簽名沒有任何關係,網上常常將摘要和簽名混爲一談,這是錯誤的。簽名和數字簽名是同一個概念,是指信息的發送者用自己的私鑰對消息摘要加密產生一個字符串,加密算法確保別人無法僞造生成這段字符串,這段數字串也是對信息的發送者發送信息真實性的一個有效證明。其他發送者用他們的私鑰對同一個消息摘要加密會得到不同的簽名,接收者只有使用發送者簽名時使用的私鑰對應的公鑰解密簽名數據才能得到消息摘要,否則得到的不是正確的消息摘要。

數字簽名是非對稱密鑰加密技術+數字摘要技術的結合。

數字簽名技術是將信息摘要用發送者的私鑰加密,和原文以及公鑰一起傳送給接收者。接收者只有用發送者的公鑰才能解密被加密的信息摘要,然後接收者用相同的Hash函數對收到的原文產生一個信息摘要,與解密的信息摘要做比對。如果相同,則說明收到的信息是完整的,在傳輸過程中沒有被修改;不同則說明信息被修改過,因此數字簽名能保證信息的完整性。並且由於只有發送者纔有加密摘要的私鑰,所以我們可以確定信息一定是發送者發送的。

另外還需要理解一個概念:數字證書。數字證書是一個經證書授權中心數字簽名的包含公鑰及其擁有者信息的文件。數字證書的格式普遍採用的是X.509V3國際標準,一個標準的X.509數字證書包含以下一些內容:證書的版本信息:

    1)證書的序列號,每個證書都有一個唯一的證書序列號; 2)證書所使用的簽名算法; 3)證書的發行機構名稱,命名規則一般採用X.500格式; 4)證書的有效期,通用的證書一般採用UTC時間格式,它的計時範圍爲1950-2049; 5)證書所有人的名稱,命名規則一般採用X.500格式; 6)證書所有人的公開密鑰; 7)證書發行者對證書的簽名。

CERT.RSA包含了數字簽名以及開發者的數字證書。CERT.RSA裏的數字簽名是指對CERT.SF的摘要採用私鑰加密後的數據,Android系統安裝apk時會對CERT.SF計算摘要,然後使用CERT.RSA裏的公鑰對CERT.RSA裏的數字簽名解密得到一個摘要,比較這兩個摘要便可知道該apk是否有正確的簽名,也就說如果其他人修改了apk並沒有重新簽名是會被檢查出來的。

需注意Android平臺的證書是自簽名的,也就說不需要權威機構簽發,數字證書的發行機構和所有人是相同的,都是開發者自己,開發者生成公私鑰對後不需要提交到權威機構進行校驗。

簽名工具的使用

Android源碼編譯出來的signapk.jar既可給apk簽名,也可給rom簽名的。使用格式:

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

-w 是指對ROM簽名時需使用的參數

publickey.x509[.pem] 是公鑰文件

privatekey.pk8 是指 私鑰文件

input.jar 要簽名的apk或者rom

output.jar 簽名後生成的apk或者rom

signapk.java

1)main函數

main函數會生成公鑰對象和私鑰對象,並調用addDigestsToManifest函數生成清單對象Manifest後,再調用signFile簽名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  public static void main(String[] args) {
  //...
  boolean signWholeFile = false;
  int argstart = 0;
  /*如果對ROM簽名需傳遞-w參數*/
  if (args[0].equals("-w")) { 
      signWholeFile = true;
      argstart = 1;
  } 
// ...
  try {
      File publicKeyFile = new File(args[argstart+0]);
      X509Certificate publicKey = readPublicKey(publicKeyFile);
      PrivateKey privateKey = readPrivateKey(new File(args[argstart+1]));
      inputJar = new JarFile(new File(args[argstart+2]), false);  
      outputFile = new FileOutputStream(args[argstart+3]);
      /*對ROM簽名,讀者可自行分析,和Apk餓簽名類似,但是它會添加otacert文件*/
      if (signWholeFile) {
          SignApk.signWholeFile(inputJar, publicKeyFile, publicKey, 
             privateKey, outputFile);
      }
      else {
          JarOutputStream outputJar = new JarOutputStream(outputFile);
          outputJar.setLevel(9);
          /*addDigestsToManifest會生成Manifest對象,然後調用signFile進行簽名*/
          signFile(addDigestsToManifest(inputJar), inputJar, 
            publicKeyFile, publicKey, privateKey, outputJar);
          outputJar.close();
      }
  } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
  } finally {
       //...
  }
}

2)addDigestsToManifest

首先我們得理解Manifest文件的結構,Manifest文件裏用空行分割成多個段,每個段由多個屬性組成,第一個段的屬性集合稱爲主屬性集合,其它段稱爲普通屬性集合,普通屬性集合一般會有Name屬性,作爲該屬性集合所在段的名字。Android的manifeset文件會爲zip的所有文件各自建立一個段,這個段的Name屬性的值就是該文件的path+文件名,另外還有一個SHA1-Digest的屬性,該屬性的值是對文件的sha1摘要用base64編碼得到的字符串。

Manifest示例:

1
2
3
4
5
6
7
8
9
10
11
Manifest-Version: 1.0
Created-By: 1.6.0-rc (Sun Microsystems Inc.)

Name: res/drawable-hdpi/user_logout.png
SHA1-Digest: zkQSZbt3Tqc9myEVuxc1dzMDPCs=

Name: res/drawable-hdpi/contacts_cancel_btn_pressed.png
SHA1-Digest: mSVZvKpvKpmgUJ9oXDJaTWzhdic=

Name: res/drawable/main_head_backgroud.png
SHA1-Digest: fe1yzADfDGZvr0cyIdNpGf/ySio=

Manifest-Version屬性和Created-By所在的段就是主屬性集合,其它屬性集合就是普通屬性集合,這些普通屬性集合都有Name屬性,作爲該段的名字。

addDigestsToManifest源代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private static Manifest addDigestsToManifest(JarFile jar)
            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 = MessageDigest.getInstance("SHA1");
    byte[] buffer = new byte[4096];
    int num; 
    // We sort the input entries by name, and add them to the
    // output manifest in sorted order.  We expect that the output
    // map will be deterministic. 
    TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();

    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();
        if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
            !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
            !name.equals(OTACERT_NAME) &&
            (stripPattern == null ||
             !stripPattern.matcher(name).matches())) {
            InputStream data = jar.getInputStream(entry);
            /*計算sha1*/
            while ((num = data.read(buffer)) > 0) {
                md.update(buffer, 0, num);
            }
            Attributes attr = null;
            if (input != null) attr = input.getAttributes(name);
            attr = attr != null ? new Attributes(attr) : new Attributes();
            /*base64編碼sha1值得到SHA1-Digest屬性的值*/
            attr.putValue("SHA1-Digest",
                          new String(Base64.encode(md.digest()), "ASCII"));
            output.getEntries().put(name, attr);
        }
    } 
 return output;
}

3)signFile

先將inputjar的所有文件拷貝至outputjar,然後生成Manifest.MF,CERT.SF和CERT.RSA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void signFile(Manifest manifest, JarFile inputJar, 
File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey,
 JarOutputStream outputJar) throws Exception {
    // Assume the certificate is valid for at least an hour.
    long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
    JarEntry je;
    // 拷貝文件
    copyFiles(manifest, inputJar, outputJar, timestamp);
    // 生成MANIFEST.MF
    je = new JarEntry(JarFile.MANIFEST_NAME);
    je.setTime(timestamp);
    outputJar.putNextEntry(je);
    manifest.write(outputJar);
    // 調用writeSignatureFile 生成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);
}

4)writeSignatureFile

生成CERT.SF,其實是對MANIFEST.MF的各個段再次計算Sha1摘要得到CERT.SF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static void writeSignatureFile(Manifest manifest, OutputStream out)
      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)"); 
    MessageDigest md = MessageDigest.getInstance("SHA1");
    PrintStream print = new PrintStream(
            new DigestOutputStream(new ByteArrayOutputStream(), md),
            true, "UTF-8"); 
    // 添加Manifest.mf的sha1摘要
    manifest.write(print);
    print.flush();
    main.putValue("SHA1-Digest-Manifest",
                  new String(Base64.encode(md.digest()), "ASCII")); 
    //對MANIFEST.MF的各個段計算sha1摘要
    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("SHA1-Digest",
                        new String(Base64.encode(md.digest()), "ASCII"));
        sf.getEntries().put(entry.getKey(), sfAttr);
    } 
    CountOutputStream cout = new CountOutputStream(out);
    sf.write(cout); 
    // A bug in the java.util.jar implementation of Android platforms
    // up to version 1.6 will cause a spurious IOException to be thrown
    // if the length of the signature file is a multiple of 1024 bytes.
    // As a workaround, add an extra CRLF in this case.
    if ((cout.size() % 1024) == 0) {
        cout.write('\\r');
        cout.write('\\n');
    }
}

5)writeSignatureBlock

採用SHA1withRSA算法對CERT.SF計算摘要並加密得到數字簽名,使用的私鑰是privateKey,然後將數字簽名和公鑰一起存入CERT.RSA。這裏使用了開源庫bouncycastle來簽名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();
  //簽名算法是SHA1withRSA
  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());
}

總結

通過源碼分析可知使用signpak.jar對某個apk或者rom簽名後,將在apk或者rom的zip根目錄添加META-INF目錄,並在裏面添加三個文件,清單文件MANIFEST.MF,對清單文件各個段計算sha1摘要得到的CERT.SF,存放數字簽名和數字證書的CERT.RSA文件。如果是rom包還會添加文件META-INF/com/android/otacert。

很多公司都定製了signapk.jar工具,簽名後放在META-INF下的文件不是CERT.SF和CERT.RSA,而是其它名字,比如微信,META-INF下的文件是COM_TENC.SF和COM_TENC.RSA。但內容和標準的CERT.SF,CERT.RSA其實是一樣的。

參考資料:

APK簽名原理:http://blog.csdn.net/kickxxx/article/details/18252881

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章