網上已有多篇分析簽名的類似文章,但是都有一個共同的問題,就是概念混亂,混亂的一塌糊塗。
在瞭解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包含了簽名和簽名用到的證書。並且要求這個證書是自簽名的。