1. 程式人生 > >安卓的數字簽名技術

安卓的數字簽名技術

什麼是簽名

實際生活中我們自己的簽名是為了證明簽名的材料是出自於你手,是否有人更改,是辨別真假的一種最簡單直接的方式。那麼什麼是數字世界的簽名呢?其實和現實世界的簽名一樣,是為了保證數字內容的完整性,保證傳輸的內容沒有經過非法的更改。在弄清楚簽名之前,需要知道什麼只摘要和什麼是非對稱加密。

  • 什麼是摘要

    摘要是指採用單向Hash函式對資料進行計算生成的固定長度的Hash值,摘要演算法有Md5,Sha1等,Md5生成的Hash值是128位的數字,即16個位元組,用十六進位制表示是32個字元,Sha1生成的Hash值是160位的數字,即20個位元組,用十六進位制表示是40個字元。我們是不能通過摘要推算出用於計算摘要的資料,如果修改了資料,那麼它的摘要一定會變化(其實這句話並不正確,只是很難正好找到不同的資料,而他們的摘要值正好相等)。摘要實際上是通過校驗演算法生成的校驗值。

  • 什麼是非對稱加密

    對於一份資料,通過一種演算法,基於傳入的金鑰(一串由數字或字元組成的字串,也稱“key”),將明文資料轉換成了不可閱讀的密文,這是眾所周知的“加密”,同樣的,密文到達目的地後,需要再以相應的演算法,配合一個金鑰,將密文再解密成明文,這就是“解密”。如果加密和解密使用的是同一個金鑰,那麼這就是“對稱金鑰加解密”(最常見的對稱加密演算法是DES)。如果加密和解密使用的是兩個不同的金鑰,那麼這就是“非對稱金鑰加解密”(最常用的非對稱加密演算法是RSA)。這兩個不同的金鑰一個叫作公開金鑰(publickey)另一個叫私有金鑰(privatekey),公開金鑰對外公開,任何人均可獲取,而私有金鑰則由自己儲存,其實公鑰和私鑰並沒有什麼不同之處,公鑰之所以成為公鑰是因為它會被公開出來,產生任意份拷貝,供任何人獲取,而只有服務主機持有唯一的一份私鑰,

    從這種分發模式上看,我們不難看出其中的用意,這種分發模式實際上是Web站點多客戶端(瀏覽器)與單一伺服器的網路拓撲所決定的,多客戶端意味著金鑰能被複制和公開獲取,單一伺服器意味著金鑰被嚴格控制,只能由本伺服器持有,這實際上也是後面要提到的之所以能通過資料證書確定信任主機的重要原因之一。如果我們跳出web站點的拓撲環境,其實就沒有什麼公鑰與私鑰之分了,比如,對於那些使用以金鑰為身份認證的SSH主機,往往是為每一個使用者單獨生成一個私鑰分發給他們自己儲存,SSH主機會儲存一份公鑰,公鑰私鑰各有一份,都不會公開傳播。

  • 數字簽名
    數字簽名是非對稱金鑰加密技術+數字摘要技術的結合。
    數字簽名技術是將資訊摘要用傳送者的私鑰加密,和原文以及公鑰一起傳送給接收者。接收者只有用傳送者的公鑰才能解密被加密的資訊摘要,然後接收者用相同的Hash函式對收到的原文產生一個資訊摘要,與解密的資訊摘要做比對。如果相同,則說明收到的資訊是完整的,在傳輸過程中沒有被修改;不同則說明資訊被修改過,因此數字簽名能保證資訊的完整性。

  • 數字證書
    從上面的描述不知是否看出了數字簽名的漏洞?漏洞確實是存在的,現在安卓的釋出的app都需要簽名,但是仍然阻止不了惡意使用者更改app,在其中注入廣告資訊。主要原因是因為廣大的使用者不知道釋出者的簽名公匙,在釋出過程中被惡意使用者重現簽名。這裡的問題就是:對於接受方來說,它怎麼能確定它所得到的公鑰一定是從目標主機那裡釋出的,而且沒有被篡改過呢?亦或者請求的目標主機本本身就從事竊取使用者資訊的不正當行為呢?這時候,我們需要有一個權威的值得信賴的第三方機構(一般是由政府稽核並授權的機構)來統一對外發放主機機構的公鑰,只要請求方這種機構獲取公鑰,就避免了上述問題的發生。這種機構被稱為證書權威機構(Certificate Authority, CA),它們所發放的包含主機機構名稱、公鑰在內的檔案就是人們所說的“數字證書”。

安卓app包中CERT.RSA包含了數字簽名以及開發者的數字證書。CERT.RSA裡的數字簽名是指對CERT.SF的摘要採用私鑰加密後的資料,Android系統安裝apk時會對CERT.SF計算摘要,然後使用CERT.RSA裡的公鑰對CERT.RSA裡的數字簽名解密得到一個摘要,比較這兩個摘要便可知道該apk是否有正確的簽名,也就說如果其他人修改了apk並沒有重新簽名是會被檢查出來的,所以只要是開發者的數字證書不是由權威機構釋出的,那麼app都有可能被惡意更改。
安卓的rom和app都會簽名,但這裡只介紹app的簽名,rom的簽名大同小異。

APK簽名分析

簽名的app用zip解壓後在META-INF資料夾,可以看到3個檔案:CERT.RSA,CERT.SF,MANIFEST.MF。其中CERT.RSA包含了公鑰資訊和釋出機構資訊;MANIFEST.MF中儲存了除自身以外所有其他檔案的SHA-1並進行base64編碼後的值(注意:對於xml等文字格式的資原始檔,系統先將這些文字檔案編譯成二進位制檔案,再獲取二進位制檔案的SHA-1值並進行base64編碼);CERT.SF的生成過程分兩步:(1)讀取MANIFEST.MF檔案的SHA-1,然後將該SHA-1值base64編碼後儲存為SHA1-Digest-Manifest,所以與MANIFEST.MF檔案相比,CERT.SF會多出一個SHA1-Digest-Manifest值,(2)然後再逐個讀取MANIFEST.MF中每個資原始檔的Name和SHA1-Digest值再新增2個空行後進行SHA-1,將該SHA-1值進行base64編碼後依次寫到CERT.SF中。下面小弟就舉個栗子,說明MANIFEST.MF檔案某個段落生成的過程。

  • MANIFEST.MF檔案某個段落生成的過程
Manifest-Version: 1.0
Created-By: 1.0 (Android)

Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=

Name: res/menu/main.xml
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=

Name: AndroidManifest.xml
SHA1-Digest: y/7aEvobcE5aOBCKiY3/s2FZW+E=

Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=

Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=

Name: res/layout/activity_main.xml
SHA1-Digest: gRYsZVPTn8/DcnOy9mOzi9B6Xkk=

Name: resources.arsc
SHA1-Digest: gYQMdJlPGAPJ5/SozzlgV2Wq57c=

Name: classes.dex
SHA1-Digest: ILBp4DCBpOWwSXlwBHXMWy5qvuk=

Name: res/drawable-xxhdpi/ic_launcher.png
SHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=

我們主要看

Name: res/menu/main.xml
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=

SHA1-Digest是如何計算出來的,首先請百度下載HashTab外掛,再在解壓的apk目錄找到res/menu/main.xml檔案,檢視屬性
這裡寫圖片描述
copy SHA-1的值08C8224A6F1D29E25B980868C21C45B263401EA8,開啟線上base64編碼輸入值後點擊convert,可以看到結果跟SHA1-Digest的值完全一致。
這裡寫圖片描述

  • CERT.SF段落生成過程
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: ztxNXNSX0ROqR+h+Y/PEbSQlYV4=

Name: res/menu/main.xml
SHA1-Digest: phti43JrVCPvFyPVD+A0aH/F804=

Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=

Name: AndroidManifest.xml
SHA1-Digest: iN6TTbqeTdXxrIq1mW2FGVdURKI=

Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=

Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=

Name: res/layout/activity_main.xml
SHA1-Digest: 05FHZzNfWjGzOvVdzEN/0nKgob8=

Name: resources.arsc
SHA1-Digest: CEgnN+t+o8xQ29Z4eYnCBCE2P4k=

Name: classes.dex
SHA1-Digest: fPUytYQBSWwxJ+W4gUUivC8/Ez4=

Name: res/drawable-xxhdpi/ic_launcher.png
SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=

下面我們主要看
Name: res/menu/main.xml
SHA1-Digest: phti43JrVCPvFyPVD+A0aH/F804=

是如何生成的。


Name: res/menu/main.xml
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=
copy到新建文字中,並再新增兩個空行,如下如所示:
這裡寫圖片描述
儲存後檢視HashTab屬性
如下圖所示:
這裡寫圖片描述
copy sha-1值線上base64編碼,如下圖所示:
這裡寫圖片描述
從結果可以看出完全和CERT.SF相關段落一致。

APK簽名分析原始碼分析

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的實現過程:

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 {
       //...
  }
}

main函式會生成公鑰物件和私鑰物件,並呼叫addDigestsToManifest函式生成清單物件Manifest後,再呼叫signFile簽名。上面已經較詳細的介紹了Manifest檔案的結構,Manifest檔案裡用空行分割成多個段,每個段由多個屬性組成,第一個段的屬性集合稱為主屬性集合,其它段稱為普通屬性集合,普通屬性集合一般會有Name屬性,作為該屬性集合所在段的名字。Android的manifeset檔案會為zip的所有檔案各自建立一個段,這個段的Name屬性的值就是該檔案的path+檔名,另外還有一個SHA1-Digest的屬性,該屬性的值是對檔案的sha1摘要用base64編碼得到的字串。
從上面的Manifest示例可以看出:
Manifest-Version屬性和Created-By所在的段就是主屬性集合,其它屬性集合就是普通屬性集合,這些普通屬性集合都有Name屬性,作為該段的名字。
addDigestsToManifest原始碼分析:

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;
}

signFile原始碼分析:
先將inputjar的所有檔案拷貝至outputjar,然後生成Manifest.MF,CERT.SF和CERT.RSA

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);
}

writeSignatureFile原始碼分析:
生成CERT.SF,其實是對MANIFEST.MF的各個段再次計算Sha1摘要得到CERT.SF。

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');
    }
}

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

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());
}