Android爲了保證系統及應用的安全性,在安裝APK的時候須要校驗包的完整性,同時,對於覆蓋安裝的場景還要校驗新舊是否匹配,這二者都是經過Android簽名機制來進行保證的,本文就簡單看下Android的簽名與校驗原理,分一下幾個部分分析下:html
簽名是摘要與非對稱密鑰加密相相結合的產物,摘要就像內容的一個指紋信息,一旦內容被篡改,摘要就會改變,簽名是摘要的加密結果,摘要改變,簽名也會失效。Android APK簽名也是這個道理,若是APK簽名跟內容對應不起來,Android系統就認爲APK內容被篡改了,從而拒絕安裝,以保證系統的安全性。目前Android有三種簽名V一、V2(N)、V3(P),本文只看前兩種V1跟V2,對於V3的輪密先不考慮。先看下只有V1簽名後APK的樣式:java
再看下只有V2簽名的APK包樣式:android
同時具備V1 V2簽名:算法
能夠看到,若是隻有V2簽名,那麼APK包內容幾乎是沒有改動的,META_INF中不會有新增文件,按Google官方文檔:在使用v2簽名方案進行簽名時,會在APK文件中插入一個APK簽名分塊,該分塊位於zip中央目錄部分以前並緊鄰該部分。在APK簽名分塊內,簽名和簽名者身份信息會存儲在APK簽名方案v2分塊中,保證整個APK文件不可修改,以下圖:安全
而V1簽名是經過META-INF中的三個文件保證簽名及信息的完整性:微信
V1簽名是如何保證信息的完整性呢?V1簽名主要包含三部份內容,若是狹義上說簽名跟公鑰的話,僅僅在.rsa文件中,V1簽名的三個文件實際上是一套機制,不能單單拿一個來講事,oracle
MANIFEST.MF:摘要文件,存儲文件名與文件SHA1摘要(Base64格式)鍵值對,格式以下,其主要做用是保證每一個文件的完整性app
若是對APK中的資源文件進行了替換,那麼該資源的摘要一定發生改變,若是沒有修改MANIFEST.MF中的信息,那麼在安裝時候V1校驗就會失敗,沒法安裝,不過若是篡改文件的同時,也修改其MANIFEST.MF中的摘要值,那麼MANIFEST.MF校驗就能夠繞過。ide
CERT.SF:二次摘要文件,存儲文件名與MANIFEST.MF摘要條目的SHA1摘要(Base64格式)鍵值對,格式以下函數
CERT.SF我的以爲有點像冗餘,更像對文件完整性的二次保證,同繞過MANIFEST.MF同樣,.SF校驗也很容易被繞過。
CERT.RSA 證書(公鑰)及簽名文件,存儲keystore的公鑰、發行信息、以及對CERT.SF文件摘要的簽名信息(利用keystore的私鑰進行加密過)
CERT.RSA與CERT.SF是相互對應的,二者名字前綴必須一致,不知道算不算一個無聊的標準。看下CERT.RSA文件內容:
CERT.RSA文件裏面存儲了證書公鑰、過時日期、發行人、加密算法等信息,根據公鑰及加密算法,Android系統就能計算出CERT.SF的摘要信息,其嚴格的格式以下:
從CERT.RSA中,咱們能獲的證書的指紋信息,在微信分享、第三方SDK申請的時候常常用到,其實就是公鑰+開發者信息的一個簽名:
除了CERT.RSA文件,其他兩個簽名文件其實跟keystore沒什麼關係,主要是文件自身的摘要及二次摘要,用不一樣的keystore進行簽名,生成的MANIFEST.MF與CERT.SF都是同樣的,不一樣的只有CERT.RSA簽名文件。也就是說前二者主要保證各個文件的完整性,CERT.RSA從總體上保證APK的來源及完整性,不過META_INF中的文件不在校驗範圍中,這也是V1的一個缺點。V2簽名又是如何保證信息的完整性呢?
V2簽名塊如何保證APK的完整性
前面說過V1簽名中文件的完整性很容易被繞過,能夠理解單個文件完整性校驗的意義並非很大,安裝的時候反而耗時,不如採用更加簡單的便捷的校驗方式。V2簽名就不針對單個文件校驗了,而是針對APK進行校驗,將APK分紅1M的塊,對每一個塊計算值摘要,以後針對全部摘要進行摘要,再利用摘要進行簽名。
也就是說,V2摘要簽名分兩級,第一級是對APK文件的一、3 、4 部分進行摘要,第二級是對第一級的摘要集合進行摘要,而後利用祕鑰進行簽名。安裝的時候,塊摘要能夠並行處理,這樣能夠提升校驗速度。
APK是先摘要,再簽名,先看下摘要的定義:Message Digest:摘要是對消息數據執行一個單向Hash,從而生成一個固定長度的Hash值,這個值就是消息摘要,至於常聽到的MD五、SHA1都是摘要算法的一種。理論上說,摘要必定會有碰撞,但只要保證有限長度內碰撞率很低就能夠,這樣就能利用摘要來保證消息的完整性,只要消息被篡改,摘要必定會發生改變。可是,若是消息跟摘要同時被修改,那就無從得知了。
而數字簽名是什麼呢(公鑰數字簽名),利用非對稱加密技術,經過私鑰對摘要進行加密,產生一個字符串,這個字符串+公鑰證書就能夠看作消息的數字簽名,如RSA就是經常使用的非對稱加密算法。在沒有私鑰的前提下,非對稱加密算法能確保別人沒法僞造簽名,所以數字簽名也是對發送者信息真實性的一個有效證實。不過因爲Android的keystore證書是自簽名的,沒有第三方權威機構認證,用戶能夠自行生成keystore,Android簽名方案沒法保證APK不被二次簽名。
知道了摘要跟簽名的概念後,再來看看Android的簽名文件怎麼來的?如何影響原來APK包?經過sdk中的apksign來對一個APK進行簽名的命令以下:
./apksigner sign --ks keystore.jks --ks-key-alias keystore --ks-pass pass:XXX --key-pass pass:XXX --out output.apk input.apk
複製代碼
其主要實如今 android/platform/tools/apksig 文件夾中,主體是ApkSigner.java的sign函數,函數比較長,分幾步分析
private void sign(
DataSource inputApk,
DataSink outputApkOut,
DataSource outputApkIn)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
// Step 1. Find input APK's main ZIP sections
ApkUtils.ZipSections inputZipSections;
<!--根據zip包的結構,找到APK中包內容Object-->
try {
inputZipSections = ApkUtils.findZipSections(inputApk);
...
複製代碼
先來看這一步,ApkUtils.findZipSections,這個函數主要是解析APK文件,得到ZIP格式的一些簡單信息,並返回一個ZipSections,
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
...
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
long cdEndOffset = cdStartOffset + cdSizeBytes;
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
return new ZipSections(
cdStartOffset,
cdSizeBytes,
cdRecordCount,
eocdOffset,
eocdBuf);
}
複製代碼
ZipSections包含了ZIP文件格式的一些信息,好比中央目錄信息、中央目錄結尾信息等,對比到zip文件格式以下:
獲取到 ZipSections以後,就能夠進一步解析APK這個ZIP包,繼續走後面的簽名流程,
long inputApkSigningBlockOffset = -1;
DataSource inputApkSigningBlock = null;
<!--檢查V2簽名是否存在-->
try {
Pair<DataSource, Long> apkSigningBlockAndOffset =
V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
inputApkSigningBlock = apkSigningBlockAndOffset.getFirst();
inputApkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
} catch (V2SchemeVerifier.SignatureNotFoundException e) {
<!--V2簽名不存在也沒什麼問題,非必須-->
}
<!--獲取V2簽名之外的信息區域-->
DataSource inputApkLfhSection =
inputApk.slice(
0,
(inputApkSigningBlockOffset != -1)
? inputApkSigningBlockOffset
: inputZipSections.getZipCentralDirectoryOffset());
複製代碼
能夠看到先進行了一個V2簽名的檢驗,這裏是用來簽名,爲何先檢驗了一次?第一次簽名的時候會直接走這個異常邏輯分支,重複簽名的時候才能獲到取以前的V2簽名,懷疑這裏獲取V2簽名的目的應該是爲了排除V2簽名,並獲取V2簽名之外的數據塊,由於簽名自己不能被算入到簽名中,以後會解析中央目錄區,構建一個DefaultApkSignerEngine用於簽名
<!--解析中央目錄區,目的是爲了解析AndroidManifest-->
// Step 2. Parse the input APK's ZIP Central Directory
ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
List<CentralDirectoryRecord> inputCdRecords =
parseZipCentralDirectory(inputCd, inputZipSections);
// Step 3. Obtain a signer engine instance
ApkSignerEngine signerEngine;
if (mSignerEngine != null) {
signerEngine = mSignerEngine;
} else {
// Construct a signer engine from the provided parameters
...
List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
new ArrayList<>(mSignerConfigs.size());
<!--通常就一個-->
for (SignerConfig signerConfig : mSignerConfigs) {
engineSignerConfigs.add(
new DefaultApkSignerEngine.SignerConfig.Builder(
signerConfig.getName(),
signerConfig.getPrivateKey(),
signerConfig.getCertificates())
.build());
}
<!--默認V1 V2都啓用-->
DefaultApkSignerEngine.Builder signerEngineBuilder =
new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
.setV1SigningEnabled(mV1SigningEnabled)
.setV2SigningEnabled(mV2SigningEnabled)
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved);
if (mCreatedBy != null) {
signerEngineBuilder.setCreatedBy(mCreatedBy);
}
signerEngine = signerEngineBuilder.build();
}
複製代碼
先解析中央目錄區,獲取AndroidManifest文件,獲取minSdkVersion(影響簽名算法),並構建DefaultApkSignerEngine,默認狀況下V1 V2簽名都是打開的。
// Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
<!--忽略這一步-->
if (inputApkSigningBlock != null) {
signerEngine.inputApkSigningBlock(inputApkSigningBlock);
}
// Step 5. Iterate over input APK's entries and output the Local File Header + data of those
// entries which need to be output. Entries are iterated in the order in which their Local
// File Header records are stored in the file. This is to achieve better data locality in
// case Central Directory entries are in the wrong order.
List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
new ArrayList<>(inputCdRecords);
Collections.sort(
inputCdRecordsSortedByLfhOffset,
CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
int lastModifiedDateForNewEntries = -1;
int lastModifiedTimeForNewEntries = -1;
long inputOffset = 0;
long outputOffset = 0;
Map<String, CentralDirectoryRecord> outputCdRecordsByName =
new HashMap<>(inputCdRecords.size());
...
// Step 6. Sort output APK's Central Directory records in the order in which they should
// appear in the output
List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
String entryName = inputCdRecord.getName();
CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
if (outputCdRecord != null) {
outputCdRecords.add(outputCdRecord);
}
}
複製代碼
第五步與第六步的主要工做是:apk的預處理,包括目錄的一些排序之類的工做,應該是爲了更高效處理簽名,預處理結束後,就開始簽名流程,首先作的是V1簽名(默認存在,除非主動關閉):
// Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
// Header + data entries and add to the list of output Central Directory records.
ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
signerEngine.outputJarEntries();
if (outputJarSignatureRequest != null) {
if (lastModifiedDateForNewEntries == -1) {
lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
lastModifiedTimeForNewEntries = 0;
}
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
outputJarSignatureRequest.getAdditionalJarEntries()) {
String entryName = entry.getName();
byte[] uncompressedData = entry.getData();
ZipUtils.DeflateResult deflateResult =
ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
byte[] compressedData = deflateResult.output;
long uncompressedDataCrc32 = deflateResult.inputCrc32;
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
signerEngine.outputJarEntry(entryName);
if (inspectEntryRequest != null) {
inspectEntryRequest.getDataSink().consume(
uncompressedData, 0, uncompressedData.length);
inspectEntryRequest.done();
}
long localFileHeaderOffset = outputOffset;
outputOffset +=
LocalFileRecord.outputRecordWithDeflateCompressedData(
entryName,
lastModifiedTimeForNewEntries,
lastModifiedDateForNewEntries,
compressedData,
uncompressedDataCrc32,
uncompressedData.length,
outputApkOut);
outputCdRecords.add(
CentralDirectoryRecord.createWithDeflateCompressedData(
entryName,
lastModifiedTimeForNewEntries,
lastModifiedDateForNewEntries,
uncompressedDataCrc32,
compressedData.length,
uncompressedData.length,
localFileHeaderOffset));
}
outputJarSignatureRequest.done();
}
// Step 8. Construct output ZIP Central Directory in an in-memory buffer
long outputCentralDirSizeBytes = 0;
for (CentralDirectoryRecord record : outputCdRecords) {
outputCentralDirSizeBytes += record.getSize();
}
if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
throw new IOException(
"Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+ " bytes");
}
ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
for (CentralDirectoryRecord record : outputCdRecords) {
record.copyTo(outputCentralDir);
}
outputCentralDir.flip();
DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
long outputCentralDirStartOffset = outputOffset;
int outputCentralDirRecordCount = outputCdRecords.size();
// Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
ByteBuffer outputEocd =
EocdRecord.createWithModifiedCentralDirectoryInfo(
inputZipSections.getZipEndOfCentralDirectory(),
outputCentralDirRecordCount,
outputCentralDirDataSource.size(),
outputCentralDirStartOffset);
複製代碼
步驟七、八、9均可以看作是V1簽名的處理邏輯,主要在V1SchemeSigner中處理,其中包括建立META-INFO文件夾下的一些簽名文件,更新中央目錄、更新中央目錄結尾等,流程不復雜,不在贅述,簡單流程就是:
這裏特殊提一下重複簽名的問題:對一個已經V1簽名的APK再次V1簽名不會有任何問題,原理就是:再次簽名的時候,會排除以前的簽名文件。
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
// Entries which represent directories sould not be listed in the manifest.
if (entryName.endsWith("/")) {
return false;
}
// Entries outside of META-INF must be listed in the manifest.
if (!entryName.startsWith("META-INF/")) {
return true;
}
// Entries in subdirectories of META-INF must be listed in the manifest.
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
return true;
}
// Ignored file names (case-insensitive) in META-INF directory:
// MANIFEST.MF
// *.SF
// *.RSA
// *.DSA
// *.EC
// SIG-*
String fileNameLowerCase =
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
if (("manifest.mf".equals(fileNameLowerCase))
|| (fileNameLowerCase.endsWith(".sf"))
|| (fileNameLowerCase.endsWith(".rsa"))
|| (fileNameLowerCase.endsWith(".dsa"))
|| (fileNameLowerCase.endsWith(".ec"))
|| (fileNameLowerCase.startsWith("sig-"))) {
return false;
}
return true;
}
複製代碼
能夠看到目錄、META-INF文件夾下的文件、sf、rsa等結尾的文件都不會被V1簽名進行處理,因此這裏不用擔憂屢次簽名的問題。接下來就是處理V2簽名。
// Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
// insert an APK Signing Block just before the output's ZIP Central Directory
ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
signerEngine.outputZipSections(
outputApkIn,
outputCentralDirDataSource,
DataSources.asDataSource(outputEocd));
if (outputApkSigingBlockRequest != null) {
byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
ZipUtils.setZipEocdCentralDirectoryOffset(
outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
outputApkSigingBlockRequest.done();
}
// Step 11. Output ZIP Central Directory and ZIP End of Central Directory
outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
outputApkOut.consume(outputEocd);
signerEngine.outputDone();
}
複製代碼
V2SchemeSigner處理V2簽名,邏輯比較清晰,直接對V1簽名過的APK進行分塊摘要,再集合簽名,V2簽名不會改變以前V1簽名後的任何信息,簽名後,在中央目錄前添加V2簽名塊,並更新中央目錄結尾信息,由於V2簽名後,中央目錄的偏移會再次改變:
簽名校驗的過程能夠看作簽名的逆向,只不過覆蓋安裝可能還要校驗公鑰及證書信息一致,不然覆蓋安裝會失敗。簽名校驗的入口在PackageManagerService的install裏,安裝官方文檔,7.0以上的手機優先檢測V2簽名,若是V2簽名不存在,再校驗V1簽名,對於7.0如下的手機,不存在V2簽名校驗機制,只會校驗V1,因此,若是你的App的miniSdkVersion<24(N),那麼你的簽名方式必須內含V1簽名:
校驗流程就是簽名的逆向,瞭解簽名流程便可,本文不求甚解,有興趣本身去分析,只是額外提下覆蓋安裝,覆蓋安裝除了檢驗APK本身的完整性之外,還要校驗證書是否一致只有證書一致(同一個keystore簽名),纔有可能覆蓋升級。覆蓋安裝同全新安裝相比較多了幾個校驗
這裏只關心證書部分:
// Verify: if target already has an installer package, it must
// be signed with the same cert as the caller.
if (targetPackageSetting.installerPackageName != null) {
PackageSetting setting = mSettings.mPackages.get(
targetPackageSetting.installerPackageName);
// If the currently set package isn't valid, then it's always
// okay to change it.
if (setting != null) {
if (compareSignatures(callerSignature,
setting.signatures.mSignatures)
!= PackageManager.SIGNATURE_MATCH) {
throw new SecurityException(
"Caller does not have same cert as old installer package "
+ targetPackageSetting.installerPackageName);
}
}
}
複製代碼
做者:看書的小蝸牛
僅供參考,歡迎指正