發布過Android應用的朋友們應該都知道,Android APK的發布是需要簽名的。簽名機制在Android應用和架構中有著十分重要的作用。例如,Android系統禁止更新安裝簽名不一致的APK;如果應用需要使用system許可權,必須保證APK簽名與Framework簽名一致,等等。
什麼是簽名
首先我們得知道什麼是摘要,摘要是指採用單向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平台的認證是自簽名的,也就說不需要權威機構簽發,數位憑證的發行機構和所有人是相同的,都是開發人員自己,開發人員產生公私密金鑰對後不需要提交到權威機構進行校正。
讀取簽名
某些時候需要擷取某個特定的apk(已安裝或者未安裝)的簽名資訊,如程式自檢測,可信賴的第三方檢測(應用市場),系統限定安裝
對此,有兩種實現方法
可以使用Java內建的API(主要用到的為JarFile,JarEntry,Certificate)進行擷取,還有一種方法是使用系統隱藏的API PackageParser,通過反射來使用對應的API.
但是由於安卓系統的分裂版本過多,並且不同廠商進行的修改很多,依賴反射隱藏API的方法並不能保證相容性和通用性,因此推薦使用JAVA內建API進行擷取:
/** * 從APK中讀取簽名 * @param file * @return * @throws IOException */ private static List<String> getSignaturesFromApk(File file) throws IOException { List<String> signatures=new ArrayList<String>(); JarFile jarFile=new JarFile(file); try { JarEntry je=jarFile.getJarEntry("AndroidManifest.xml"); byte[] readBuffer=new byte[8192]; Certificate[] certs=loadCertificates(jarFile, je, readBuffer); if(certs != null) { for(Certificate c: certs) { String sig=toCharsString(c.getEncoded()); signatures.add(sig); } } } catch(Exception ex) { } return signatures; } /** * 載入簽名 * @param jarFile * @param je * @param readBuffer * @return */ private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) { try { InputStream is=jarFile.getInputStream(je); while(is.read(readBuffer, 0, readBuffer.length) != -1) { } is.close(); return je != null ? je.getCertificates() : null; } catch(IOException e) { } return null; } /** * 將簽名轉成轉成可見字串 * @param sigBytes * @return */ private static String toCharsString(byte[] sigBytes) { byte[] sig=sigBytes; final int N=sig.length; final int N2=N * 2; char[] text=new char[N2]; for(int j=0; j < N; j++) { byte v=sig[j]; int d=(v >> 4) & 0xf; text[j * 2]=(char)(d >= 10 ? ('a' + d - 10) : ('0' + d)); d=v & 0xf; text[j * 2 + 1]=(char)(d >= 10 ? ('a' + d - 10) : ('0' + d)); } return new String(text); }