Android 簽名的應用 ,此文涉及內容比較全面。

先聲明,本文不是自己原創,都是在做應用的時候涉及到的內容,但是把簽名的所有內容都涉及到了。包括簽名的知識點、怎麼簽名、安裝前的簽名對比是否相同。

1、簽名的基礎知識點:

發佈過Android應用的朋友們應該都知道,Android APK的發佈是需要簽名的。簽名機制在Android應用和框架中有着十分重要的作用。

例如,Android系統禁止更新安裝簽名不一致的APK;如果應用需要使用system權限,必須保證APK簽名與Framework簽名一致,等等。在《APK Crack》 (http://www.blogjava.net/zh-weir/archive/2011/06/11/352099.html)一文中,我們瞭解到,要破解一個APK,必然需要重新對APK進行簽名。而這個簽名,一般情況無法再與APK原先的簽名保持一致。(除非APK原作者的私鑰泄漏,那已經是另一個層次的軟件安全問題了。)

簡單地說,簽名機制標明瞭APK的發行機構。因此,站在軟件安全的角度,我們就可以通過比對APK的簽名情況,判斷此APK是否由“官方”發行,而不是被破解篡改過重新簽名打包的“盜版軟件”。

Android簽名機制

爲了說明APK簽名比對對軟件安全的有效性,我們有必要了解一下Android APK的簽名機制。爲了更易於大家理解,我們從Auto-Sign工具的一條批處理命令說起。

在《APK Crack》 (http://www.blogjava.net/zh-weir/archive/2011/06/11/352099.html)一文中,我們瞭解到,要簽名一個沒有簽名過的APK,可以使用一個叫作Auto-sign的工具。Auto-sign工具實際運行的是一個叫做Sign.bat的批處理命令。用文本編輯器打開這個批處理文件,我們可以發現,實現簽名功能的命令主要是這一行命令:

java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

這條命令的意義是:通過signapk.jar這個可執行jar包,以“testkey.x509.pem”這個公鑰文件和“testkey.pk8”這個私鑰文件對“update.apk”進行簽名,簽名後的文件保存爲“update_signed.apk”。

對於此處所使用的私鑰和公鑰的生成方式,這裏就不做進一步介紹了。這方面的資料大家可以找到很多。我們這裏要講的是signapk.jar到底做了什麼。

signapk.jar是Android源碼包中的一個簽名工具。由於Android是個開源項目,所以,很高興地,我們可以直接找到signapk.jar的源碼!路徑爲/build/tools/signapk/SignApk.java。

對比一個沒有簽名的APK和一個簽名好的APK,我們會發現,簽名好的APK包中多了一個叫做META-INF的文件夾。裏面有三個文件,分別名爲MANIFEST.MF、CERT.SF和CERT.RSA。signapk.jar就是生成了這幾個文件(其他文件沒有任何改變。因此我們可以很容易去掉原有簽名信息)。

通過閱讀signapk源碼,我們可以理清簽名APK包的整個過程。

1、 生成MANIFEST.MF文件:

程序遍歷update.apk包中的所有文件(entry),對非文件夾非簽名文件的文件,逐個生成SHA1的數字簽名信息,再用Base64進行編碼。具體代碼見這個方法:

private static Manifest addDigestsToManifest(JarFile jar)

關鍵代碼如下:

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) &&
(stripPattern == null ||!stripPattern.matcher(name).matches())) {

InputStream data = jar.getInputStream(entry);
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();
attr.putValue("SHA1-Digest", base64.encode(md.digest()));
output.getEntries().put(name, attr);
}
}

之後將生成的簽名寫入MANIFEST.MF文件。關鍵代碼如下:

Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);

這裏簡單介紹下SHA1數字簽名。簡單地說,它就是一種安全哈希算法,類似於MD5算法。它把任意長度的輸入,通過散列算法變成固定長度的輸出(這裏我們稱作“摘要信息”)。你不能僅通過這個摘要信息復原原來的信息。另外,它保證不同信息的摘要信息彼此不同。因此,如果你改變了apk包中的文件,那麼在apk安裝校驗時,改變後的文件摘要信息與MANIFEST.MF的檢驗信息不同,於是程序就不能成功安裝。

2、生成CERT.SF文件:

對前一步生成的Manifest,使用SHA1-RSA算法,用私鑰進行簽名。關鍵代碼如下:

Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,
new SignatureOutputStream(outputJar, signature));

RSA是一種非對稱加密算法。用私鑰通過RSA算法對摘要信息進行加密。在安裝時只能使用公鑰才能解密它。解密之後,將它與未加密的摘要信息進行對比,如果相符,則表明內容沒有被異常修改。

3、生成CERT.RSA文件:

生成MANIFEST.MF沒有使用密鑰信息,生成CERT.SF文件使用了私鑰文件。那麼我們可以很容易猜測到,CERT.RSA文件的生成肯定和公鑰相關。

CERT.RSA文件中保存了公鑰、所採用的加密算法等信息。核心代碼如下:

je = new JarEntry(CERT_RSA_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);

其中writeSignatureBlock的代碼如下:

private static void writeSignatureBlock(
Signature signature, X509Certificate publicKey, OutputStream out)
throws IOException, GeneralSecurityException {
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());

PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] { AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { publicKey },
new SignerInfo[] { signerInfo });

pkcs7.encodeSignedData(out);
}

好了,分析完APK包的簽名流程,我們可以清楚地意識到:

1、 Android簽名機制其實是對APK包完整性和發佈機構唯一性的一種校驗機制。
2、 Android簽名機制不能阻止APK包被修改,但修改後的再簽名無法與原先的簽名保持一致。(擁有私鑰的情況除外)。
3、 APK包加密的公鑰就打包在APK包內,且不同的私鑰對應不同的公鑰。換句話言之,不同的私鑰簽名的APK公鑰也必不相同。所以我們可以根據公鑰的對比,來判斷私鑰是否一致。

APK簽名比對的實現方式

好了,通過Android簽名機制的分析,我們從理論上證明了通過APK公鑰的比對能判斷一個APK的發佈機構。並且這個發佈機構是很難僞裝的,我們暫時可以認爲是不可僞裝的。

有了理論基礎後,我們就可以開始實踐了。那麼如何獲取到APK文件的公鑰信息呢?因爲Android系統安裝程序肯定會獲取APK信息進行比對,所以我們可以通過Android源碼獲得一些思路和幫助。

源碼中有一個隱藏的類用於APK包的解析。這個類叫PackageParser,路徑爲frameworks\base\core\java\android\content\pm\PackageParser.java。當我們需要獲取APK包的相關信息時,可以直接使用這個類,下面代碼就是一個例子函數:

private PackageInfo parsePackage(String archiveFilePath, int flags){

PackageParser packageParser = new PackageParser(archiveFilePath);
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
final File sourceFile = new File(archiveFilePath);
PackageParser.Package pkg = packageParser.parsePackage(
sourceFile, archiveFilePath, metrics, 0);
if (pkg == null) {
return null;
}

packageParser.collectCertificates(pkg, 0); 

return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0);
}

其中參數archiveFilePath指定APK文件路徑;flags需設置PackageManager.GET_SIGNATURES位,以保證返回證書籤名信息。

具體如何通過PackageParser獲取簽名信息在此處不做詳述,具體代碼請參考PackageParser中的public boolean collectCertificates(Package pkg, int flags)和private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer)方法。至於如何在Android應用開發中使用隱藏的類及方法,可以參看我的這篇文章: 《Android應用開發中如何使用隱藏API》 (http://www.blogjava.net/zh-weir/archive/2011/05/01/349360.html) 。

緊接着,我們就可以通過packageInfo.signatures來訪問到APK的簽名信息。還需要說明的是 Android中Signature和Java中Certificate的對應關係。它們的關係如下面代碼所示:

pkg.mSignatures = new Signature[certs.length];
for (int i=0; i<N; i++) {
pkg.mSignatures[i] = new Signature(
certs[i].getEncoded());
}

也就是說signature = new Signature(certificate.getEncoded()); certificate證書中包含了公鑰和證書的其他基本信息。公鑰不同,證書肯定互不相同。我們可以通過certificate的getPublicKey方法獲取公鑰信息。所以比對簽名證書本質上就是比對公鑰信息。

OK,獲取到APK簽名證書之後,就剩下比對了。這個簡單,功能函數如下所示:

private boolean IsSignaturesSame(Signature[] s1, Signature[] s2) {
if (s1 == null) {
return false;
}
if (s2 == null) {
return false;
}
HashSet<Signature> set1 = new HashSet<Signature>();
for (Signature sig : s1) {
set1.add(sig);
}
HashSet<Signature> set2 = new HashSet<Signature>();
for (Signature sig : s2) {
set2.add(sig);
}
// Make sure s2 contains all signatures in s1.
if (set1.equals(set2)) {
return true;
}
return false;
}

APK簽名比對的應用場景

經過以上的論述,想必大家已經明白簽名比對的原理和我的實現方式了。那麼什麼時候什麼情況適合使用簽名對比來保障Android APK的軟件安全呢?

個人認爲主要有以下三種場景:

1、 程序自檢測。在程序運行時,自我進行簽名比對。比對樣本可以存放在APK包內,也可存放於雲端。缺點是程序被破解時,自檢測功能同樣可能遭到破壞,使其失效。

2、 可信賴的第三方檢測。由可信賴的第三方程序負責APK的軟件安全問題。對比樣本由第三方收集,放在雲端。這種方式適用於殺毒安全軟件或者APP Market之類的軟件下載市場。缺點是需要聯網檢測,在無網絡情況下無法實現功能。(不可能把大量的簽名數據放在移動設備本地)。

3、 系統限定安裝。這就涉及到改Android系統了。限定僅能安裝某些證書的APK。軟件發佈商需要向系統發佈上申請證書。如果發現問題,能追蹤到是哪個軟件發佈商的責任。適用於系統提供商或者終端產品生產商。缺點是過於封閉,不利於系統的開放性。

以上三種場景,雖然各有缺點,但缺點並不是不能克服的。例如,我們可以考慮程序自檢測的功能用native method的方法實現等等。軟件安全是一個複雜的課題,往往需要多種技術聯合使用,才能更好的保障軟件不被惡意破壞。

參考資料

Android源碼
《Android中的簽名機制》 (http://blog.csdn.net/absurd/article/details/5002763)


以上內容轉自:http://www.blogjava.net/zh-weir/archive/2011/07/19/354663.html


另附上其他的資料:

private String showUninstallAPKSignatures(String apkPath) {
 2         String PATH_PackageParser = "android.content.pm.PackageParser";
 3         try {
 4             // apk包的文件路徑
 5             // 這是一個Package 解釋器, 是隱藏的
 6             // 構造函數的參數只有一個, apk文件的路徑
 7             // PackageParser packageParser = new PackageParser(apkPath);
 8             Class pkgParserCls = Class.forName(PATH_PackageParser);
 9             Class[] typeArgs = new Class[1];
10             typeArgs[0] = String.class;
11             Constructor pkgParserCt = pkgParserCls.getConstructor(typeArgs);
12             Object[] valueArgs = new Object[1];
13             valueArgs[0] = apkPath;
14             Object pkgParser = pkgParserCt.newInstance(valueArgs);
15             MediaApplication.logD(DownloadApk.class, "pkgParser:" + pkgParser.toString());
16             // 這個是與顯示有關的, 裏面涉及到一些像素顯示等等, 我們使用默認的情況
17             DisplayMetrics metrics = new DisplayMetrics();
18             metrics.setToDefaults();
19             // PackageParser.Package mPkgInfo = packageParser.parsePackage(new
20             // File(apkPath), apkPath,
21             // metrics, 0);
22             typeArgs = new Class[4];
23             typeArgs[0] = File.class;
24             typeArgs[1] = String.class;
25             typeArgs[2] = DisplayMetrics.class;
26             typeArgs[3] = Integer.TYPE;
27             Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage",
28                     typeArgs);
29             valueArgs = new Object[4];
30             valueArgs[0] = new File(apkPath);
31             valueArgs[1] = apkPath;
32             valueArgs[2] = metrics;
33             valueArgs[3] = PackageManager.GET_SIGNATURES;
34             Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, valueArgs);
35             
36             typeArgs = new Class[2];
37             typeArgs[0] = pkgParserPkg.getClass();
38             typeArgs[1] = Integer.TYPE;
39             Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates",
40                     typeArgs);
41             valueArgs = new Object[2];
42             valueArgs[0] = pkgParserPkg;
43             valueArgs[1] = PackageManager.GET_SIGNATURES;
44             pkgParser_collectCertificatesMtd.invoke(pkgParser, valueArgs);
45             // 應用程序信息包, 這個公開的, 不過有些函數, 變量沒公開
46             Field packageInfoFld = pkgParserPkg.getClass().getDeclaredField("mSignatures");
47             Signature[] info = (Signature[]) packageInfoFld.get(pkgParserPkg);
48             MediaApplication.logD(DownloadApk.class, "size:"+info.length);
49             MediaApplication.logD(DownloadApk.class, info[0].toCharsString());
50             return info[0].toCharsString();
51         } catch (Exception e) {
52             e.printStackTrace();
53         }
54         return null;
55     }

 

獲取程序自身的簽名:

private String getSign(Context context) {
        PackageManager pm = context.getPackageManager();
        List<PackageInfo> apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);
        Iterator<PackageInfo> iter = apps.iterator();
        while(iter.hasNext()) {
             PackageInfo packageinfo = iter.next();
             String packageName = packageinfo.packageName;
             if (packageName.equals(instance.getPackageName())) {
                MediaApplication.logD(DownloadApk.class, packageinfo.signatures[0].toCharsString());
                return packageinfo.signatures[0].toCharsString();
             }
     }
        return null;
    }

 

對比2個方法的返回值來判斷APK升級包的簽名是否一致,一致就提示可以安裝。

以上轉自:http://www.cnblogs.com/tt_mc/


還有簽名命令:

命令:

生成keystone(存放一組證書和私鑰的地方)

keytool -genkey -alias(別名) hello -keyalg  RSA -validity 20000 -keystore hello.keystore

 

查看keystore 信息

keytool -list  -v -keystore hello.keystore -storepass 123456  

 

缺省情況下,-list 命令打印證書的 MD5 指紋。而如果指定了 -v 選項,將以可讀格式打印證書,如果指定了 -rfc 選項,將以可打印的編碼格式輸出證書。

keytool -list  -rfc -keystore  hello.keystore -storepass 123456

 

證書的導出:

keytool -export -alias test -keystore  hello.keystore -file  test.crt -storepass 123456

 

證書的導入

keytool -import -alias rootcert -file root.crt  -keystore hello.keystore

 

證書條目的刪除:

keytool -delete -alias ceatecert1 -keystore .keystore -storepass 123456

 

 

使用jdk的jarsigner工具對apk文件簽名

 jarsigner -verbose -keystore hello.keystore TestApk.apk test

 

 

簽名後可以使用如下命令驗證是否簽名成功: 

 jarsigner -verify to_sign.apk 

如果需要查看更詳細的驗證信息,可修改爲:

 jarsigner -certs -verbose -verify to_sign.apk 

以上轉自:http://yelwen000.iteye.com/blog/1207409(這裏也有獲取簽名的代碼,但是PackageParser 是android 隱藏的API ,所以我附上了上面的通過反射獲取簽名的代碼。


這幾篇 文章講的不錯 ,再次感謝你們!

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