安卓防簽名策略

標籤(空格分隔): 安卓簽名java


#1、安卓生成APK安裝包android

一、安卓打包過程

安卓打包過程可參考google給出的APK打包流程圖, ios

流程圖
最終經過apkbuilder生成的apk實際上最終的存儲就是一個zip壓縮包,所以能夠參考zip壓縮包的存儲格式來理解apk的存儲,固然apk打包前已經作了二進制處理、資源壓縮、dex轉換等操做。

二、ZipAlign

通過aapt編譯生成的APK,其實是一個有內部文件規範的zip壓縮包;能夠經過使用ZipAlign命令確保全部未壓縮的數據的開頭均相對於文件開頭部分執行特定的字節對齊,這樣可減小應用消耗的 RAM 量;可是因爲須要對數據採用邊界對齊,apk包的體積會增大,大約增長了100KB左右;算法

2、jarsigner簽名工具

一、v1簽名方案

jarSigner簽名方式由JDK提供,jarSigner簽名後生成一個META-INF文件夾。 一、MANIFEST.MF文件,這個文件包含了APK壓縮後的全部文件對應的摘要信息,每一個文件路徑和對應的摘要信息都列舉出來:安全

Name: lib/armeabi/libNativeCrashCollect.so
SHA-256-Digest: VAlz6QwJBoJ1mFMJTuzeA9sZ6m8e1QNGvE/KJ6iSa2c=

Name: res/drawable/upgrade_progress.xml
SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho=
複製代碼

二、CERT.SF文件,SF則是MF文件的摘要信息以及.MF文件當中每一個條目在用摘要算法計算獲得的摘要信息並用base64編碼保存; 三、CERT.RSA,CERT.SF文件則存放證書信息,公鑰信息,以及用私鑰對.SF文件的加密數據即簽名信息;bash

二、APK安裝安卓驗證jarsigner簽名

使用jarsigner簽名的APK安裝時候,驗證能夠參考sdk/sources/$sdkversion/android/util/jar下面的文件,驗證主要包括兩個部分,第一步經過CERT.RSA文件驗證CERT.SF文件,參考方法:app

StrictJarVerifier.java
synchronized boolean readCertificates(){
    ......
    while (it.hasNext()) {
            String key = it.next();
            if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
                verifyCertificate(key);
                it.remove();
            }
        }
}


 static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)throws GeneralSecurityException {
        ......
}
複製代碼

v1也支持多種簽名,以上只是經過解密驗證.SF文件的摘要信息是正確的;第二步是驗證.SF文件.MF文件對應的摘要信息,確保META-INF目錄下的文件沒有被篡改:dom

private void verifyCertificate(String certFile) {
    ......
    byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        // Manifest entry is required for any verifications.
        if (manifestBytes == null) {
            return;
        }
        
    ......
    // Use .SF to verify the whole manifest.
        String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
        if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, Attributes> entry = it.next();
                StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", manifestBytes,
                        chunk.start, chunk.end, createdBySigntool, false)) {
                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
                }
            }
        }
}

複製代碼

上面作了兩個事情,第一個事情經過驗證.SF文件和.MF的摘要來確認.MF文件是沒有被篡改的,而後讀取.MF文件對應的文件摘要信息,相似:ide

Name: lib/armeabi/libNativeCrashCollect.so
SHA-256-Digest: VAlz6QwJBoJ1mFMJTuzeA9sZ6m8e1QNGvE/KJ6iSa2c=

Name: res/drawable/upgrade_progress.xml
SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho=
複製代碼

而後對應確認每個文件對應的摘要信息是不是正確的,以此來確保APK解壓後的文件都沒有被修改過;函數

三、jarsigner簽名的缺陷

根據上面安卓校驗jarsigner的過程,能夠看到jarsigner簽名後的APK可能有以下問題:

一、對於META-INF文件夾,安卓只會校驗CERT.SF三個文件,如若在META-INF存放其餘文件,會逃過安卓的檢測過程,此處存在較大的安全漏洞;(參考美團經過在META-INF下添加一個空文件來表明渠道號)

二、每次安裝APK都須要經過解壓APK再作對應的校驗,解壓APK是一個耗時耗電的過程,安裝過程體驗很差;

三、對Apk重簽名成本很低,只須要刪除掉META-INF文件夾,從新簽名便可;

可是說到防二次簽名的問題,本質上並非因爲jarsigner的安全性不高,jarsigner 也用到數字簽名技術,直接破譯的成本仍是挺高的;之因此都說 jarsigner簽名容易被二次簽名,是由於Android是開源的,所以Android的簽名策略都是公開的,公開的簽名策略固然會容易被破解。

在以前jarsigner簽名階段,一樣能夠植入本身的安全策略來防止重簽名,主要思路有兩種:

1.利用壓縮包的File Comment區域,簡單來講就是壓縮包存在一個File Comment區域,顧名思義是文件註釋區域,所以在這個區間寫入信息是不影響壓縮包自己的, Android打包的APK就是一個壓縮包,往此區域寫入信息Apk不會被破壞;所以咱們能夠往這個區域植入本身定義的安全信息,在APK安裝運行時候檢測此區域的安全信息正確與否便可;

2.在META-INF下添加一個空文件寫入本身的安全信息,一樣APK運行時候檢測此區域信息;

以上兩種防二次簽名,其實是在基於Android簽名方案公開的狀況下,引入自定義的私有安全策略來達到防二次簽名的目的;

2、APK Signature Scheme v2簽名

一、apksigner簽名

Android 7.0引入了新的應用簽名方案APK Signature Schemev2,APK簽名方案v2是基於APK二進制文件的,即簽名和安裝校驗都是基於APK二進制文件的,即只要二進制文件發生改變,就認爲APK被修改了。 apksigner簽名先後APK文件內容以下:

簽名

v2簽名後在Central Directory塊前生成一個APK Signing Block,存儲的就是v2簽名和簽名者身份信息; apk簽名塊的結構以下

偏移 字節數 描述
@+0 8 這個Block的長度(本字段的長度不計算在內)
@+8 n 一組ID-value
@-24 8 這個Block的長度(和第一個字段同樣值)
@-16 16 魔數"APK Sign Block 42"

APK的v2簽名的ID-value能夠存儲多個Id-值對,其中會被校驗的"ID-值"對的ID爲0x7109871a,其餘ID未知的"ID-值"對不會被校驗; 此處能夠作爲一個漏洞,美團新的渠道包方案就是利用了這個漏洞; 經過分析安卓對於v2簽名文件的源碼可知,在簽名前,安卓生成的 APK是一個壓縮二進制文件,v2簽名後也會生成一個對應的SF文件,SF文件裏面有個標誌 X-Android-APK-Signed ,判斷是否有v2簽名這個標誌,對應命令: apksigner verify 執行這個命令的源碼其實就是:

java源碼

/**
 * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
 * associated with each signer.
 *
 * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
 * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
 *         verify.
 * @throws IOException if an I/O error occurs while reading the APK file.
 */
private static X509Certificate[][] verify(RandomAccessFile apk)
        throws SignatureNotFoundException, SecurityException, IOException {
    SignatureInfo signatureInfo = findSignature(apk);
    return verify(apk.getFD(), signatureInfo);
}

複製代碼

首先咱們須要找到對應的APK Signing Block ,話很少說,直接看源碼:

private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException {
    ......
    // Find the APK Signing Block. The block immediately precedes the Central Directory.
    long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
    Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
    ......
複製代碼

由上面源碼邏輯能夠看到,首先須要找到Central Directory,而後根據存儲結構找到前面的Signing Block,怎麼去肯定是否有生成Signing Block呢?看代碼是如何實現的?

private static Pair<ByteBuffer, Long> findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
    ......
    
    // Read the magic and offset in file from the footer section of the block:
    // * uint64: size of block
    // * 16 bytes: magic
    ByteBuffer footer = ByteBuffer.allocate(24);
    footer.order(ByteOrder.LITTLE_ENDIAN);
    apk.seek(centralDirOffset - footer.capacity());
    apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
    if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
            || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
        throw new SignatureNotFoundException(
                "No APK Signing Block before ZIP Central Directory");
    }
    ......
複製代碼

須要關注以下兩個值:

private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
複製代碼

以下真正去生成Apk Signing Block的代碼須要結合Apk的二進制小端序結構去分析,具體代碼以下:

private static Pair<ByteBuffer, Long> findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
    ......
    
    int totalSize = (int) (apkSigBlockSizeInFooter + 8);
    long apkSigBlockOffset = centralDirOffset - totalSize;
    if (apkSigBlockOffset < 0) {
        throw new SignatureNotFoundException(
                "APK Signing Block offset out of range: " + apkSigBlockOffset);
    }
    ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
    apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
    apk.seek(apkSigBlockOffset);
    apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
    long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
    ......
複製代碼

APK Signing Block APK簽名分塊裏面存儲有APK簽名方案V2分塊,關於其查找過程,能夠參考源碼

java源碼

private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException {
    ......
    // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
    ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
    ......
複製代碼

APK Signing Block 內部多字節對象存儲方式採用的是LITTLE_ENDIAN小端序; 必定要記得APK Signing Block內部的存儲內容,因爲採用小端序,前面32個字節的數據是固定的,用來存儲長度和Scheme v2分塊,因爲用來存儲ID-Value的區域是不固定的,所以整個簽名分塊的長度是未知的,所以就有對應的標誌長度的字段;對應源碼:

private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock) throws SignatureNotFoundException {
    checkByteOrderLittleEndian(apkSigningBlock);
    // FORMAT:
    // OFFSET DATA TYPE DESCRIPTION
    // * @+0 bytes uint64: size in bytes (excluding this field)
    // * @+8 bytes pairs
    // * @-24 bytes uint64: size in bytes (same as the one above)
    // * @-16 bytes uint128: magic
    ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
    ......
    
    if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
        return getByteBuffer(pairs, len - 4);
    }
    ......

    throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
}
複製代碼

仔細觀察上面的代碼,給的ByteBuffer的起始位置信息,其實這是由於目前的存儲結構是小端序,所以實際給出的ByteBuffer就是去掉ID-Value剩下的值;小黑板:在v2簽名驗證過程當中,用來存儲Id-Value的區間被過濾掉不作檢查,安卓在v2簽名也留下來給你們能夠利用的區間;

爲何會有兩個區域用來存儲簽名分塊的長度呢? 尋找APK簽名方案v2分塊的過程是以ID:0x7109871a爲標誌,找到對應的value值,這個ID標誌位很重要,其餘全部的value值都是根據這個ID索引獲得的; 而這個APK Signature Scheme v2 Block存儲的數據signer由幾部分組成,第一個是signed data存儲將APK內容按照必定規則分塊計算摘要,採用兩級樹方式,最終獲得的摘要信息;第二個是signatures存儲當前簽名所採用的簽名算法,目前能夠支持的計算摘要算法有7種,而對應的摘要算法又有對應的加密算法,所以這個字段存儲了簽名算法;第三個是帶長度前綴的public key(SubjectPublicKeyInfo,ASN.1 DER 形式),即剛剛用來加密的私鑰對應的公鑰信息;以上就是APK Signature Scheme v2 Block的數據存儲結構,能夠直觀的看下圖:

APK數據是很大,若是直接採用非對稱加密數據,效果是很是慢的,那如何作簽名呢? —— 答案是對APK受保護的數據直接按照必定規則分塊,而後對分塊分塊計算摘要,再採用兩級樹方式,將剛剛獲得的分塊摘要再按照必定規則計算獲得最終摘要;非對稱加密直接私鑰加密最終摘要信息; 如上的簽名方案是否還有加快計算速度的方案?—— 能夠先提早分塊,而後考慮並行處理計算分塊摘要,大大提升計算速度; 上面的過程對應源碼實現:

private static X509Certificate[] verifySigner(
            ByteBuffer signerBlock,
            Map<Integer, byte[]> contentDigests,
            CertificateFactory certFactory) throws SecurityException, IOException {
    ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
    ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
    byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
    ......
    //get signature,consider litte-edian
    while (signatures.hasRemaining()) {
        signatureCount++;
        try {
            ByteBuffer signature = getLengthPrefixedSlice(signatures);
            ......
            int sigAlgorithm = signature.getInt();
            signaturesSigAlgorithms.add(sigAlgorithm);
            if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
                continue;
            }
            if ((bestSigAlgorithm == -1)
                    || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
                bestSigAlgorithm = sigAlgorithm;
                bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
            }
        } catch (IOException | BufferUnderflowException e) {
            ......
        }
    }
    
    ......
    //verify signed data
    try {
        PublicKey publicKey =
                KeyFactory.getInstance(keyAlgorithm)
                        .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
        Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
        sig.initVerify(publicKey);
        if (jcaSignatureAlgorithmParams != null) {
            sig.setParameter(jcaSignatureAlgorithmParams);
        }
        sig.update(signedData);
        sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
    } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
            | InvalidAlgorithmParameterException | SignatureException e) {
        throw new SecurityException(
                "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
    }
    if (!sigVerified) {
        throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
    }
    ......
    
    //get digest of signed data
    while (digests.hasRemaining()) {
        digestCount++;
        try {
            ByteBuffer digest = getLengthPrefixedSlice(digests);
            if (digest.remaining() < 8) {
                throw new IOException("Record too short");
            }
            int sigAlgorithm = digest.getInt();
            digestsSigAlgorithms.add(sigAlgorithm);
            if (sigAlgorithm == bestSigAlgorithm) {
                contentDigest = readLengthPrefixedByteArray(digest);
            }
        } catch (IOException | BufferUnderflowException e) {
            throw new IOException("Failed to parse digest record #" + digestCount, e);
        }
    }
    ......
    //verify digest 
    int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
    byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
    if ((previousSignerDigest != null)
            && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
        throw new SecurityException(
                getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
                + " contents digest does not match the digest specified by a preceding signer");
    }
    ......
    
    //get public key
    ByteBuffer certificates = getLengthPrefixedSlice(signedData);
    List<X509Certificate> certs = new ArrayList<>();
    int certificateCount = 0;
    while (certificates.hasRemaining()) {
        certificateCount++;
        byte[] encodedCert = readLengthPrefixedByteArray(certificates);
        X509Certificate certificate;
        try {
            certificate = (X509Certificate)
                    certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
        } catch (CertificateException e) {
            throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
        }
        certificate = new VerbatimX509Certificate(certificate, encodedCert);
        certs.add(certificate);
    }

    //verify public key
    if (certs.isEmpty()) {
        throw new SecurityException("No certificates listed");
    }
    X509Certificate mainCertificate = certs.get(0);
    byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
    if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
        throw new SecurityException(
                "Public key mismatch between certificate and signature record");
    }

    return certs.toArray(new X509Certificate[certs.size()]);

複製代碼

上面的源碼比較多,可是大體的邏輯同樣,都是先獲得對應的signature、digest of signed data、public Key,而後分別都要作verify;上面作了分別verify以後,接下來要作完整性校驗,也就是驗證咱們的簽名邏輯,直接看源碼是如何處理的?

private static void verifyIntegrity( Map<Integer, byte[]> expectedDigests, FileDescriptor apkFileDescriptor, long apkSigningBlockOffset, long centralDirOffset, long eocdOffset, ByteBuffer eocdBuf) throws SecurityException {
                // We need to verify the integrity of the following three sections of the file:
    // 1. Everything up to the start of the APK Signing Block.
    // 2. ZIP Central Directory.
    // 3. ZIP End of Central Directory (EoCD).
    // Each of these sections is represented as a separate DataSource instance below.

    // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
    // avoid wasting physical memory. In most APK verification scenarios, the contents of the
    // APK are already there in the OS's page cache and thus mmap does not use additional
    // physical memory.
    DataSource beforeApkSigningBlock =
            new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
    DataSource centralDir =
            new MemoryMappedFileDataSource(
                    apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);

    // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
    // Central Directory must be considered to point to the offset of the APK Signing Block.
    eocdBuf = eocdBuf.duplicate();
    eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
    ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
    ......
    //計算最終摘要
    try {
        actualDigests =
                computeContentDigests(
                        digestAlgorithms,
                        new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
    } catch (DigestException e) {
        throw new SecurityException("Failed to compute digest(s) of contents", e);
    }
    ......
        
    }
複製代碼

computeContentDigests就是整個計算摘要的函數,具體源碼能夠自行閱讀,上面已經簡要說明其原理;如上就是APK安裝時候,安卓系統驗證是否使用v2簽名的過程;

從上面v2簽名的過程來看,其相對於jarsigner方式,想要作二次簽名就是須要熟悉v1簽名過程,考慮針對二進制文件去掉對應的APK Signing Block再從新簽名,實際上也是能夠實現的; APKSigner想對於JarSigner的優先兩個:一、簽名更快,安裝時候驗證簽名也更快,直接對二進制文件操做,而不須要像jarsigner那樣須要先壓縮文件簽名,先解壓文件驗證簽名,效率過低;二、安全性更好,用戶想要抹掉簽名從新修改文件的成本更高,須要對整個ApkSigner原理很是清楚,二次簽名的成本更高; 可是如上面所述,ApkSigner並不能防止二次簽名,要防二次簽名須要有其餘方案;

v2分塊的本質就是數字簽名的過程,所以會存儲對應的加解密信息和摘要信息; 每個APK簽名方案v2分塊對應一個簽名者/身份簽名,有多個簽名者則含有多個v2分塊;v2分塊結構信息以下: v2分塊是用來保護APK全文件的,明文即APK全文件信息,v2分塊存儲了摘要算法和摘要信息,同時存儲了數字證書信息和加密算法信息,並提供公鑰信息;相似數字簽名的校驗一致,最終經過計算就可以證實APK已經作了v2簽名,且apk內容沒有被篡改; 目前APK Signature算法支持的主流的摘要算法,同時支持RSA、DSA、EC橢圓加密等非對稱算法; APK Signature算法實質是經過對APK全內容作相似數字簽名的工做,來保證APK文件不會被篡改。

二、APK Signature保護APK內容的實現

首先打包的APK轉換成zip其文件結構以下: ?問題1:能夠把APK看成Zip文件來處理,可是Zip結構是有幾個限制條件的,好比zipcommentfield對應到什麼內容,zip eocd會有 comment field?

APK Signature算法主要作了兩個事情: a,計算第一、三、4部份內容的摘要,將這些摘要信息存儲到APK Signing Block的v2分塊的signed data分塊;(?是對最終的頂級摘要加密簽名仍是對每一個分塊摘要都加密簽名————最終計算獲得的頂級摘要信息存儲到singed data分塊,由於最終只是按照規則計算最終摘要相同便可;) b,將上面獲得的分塊(摘要信息)經過一個或多個加密算法來加密;(?對頂級摘要可能採用一個或多個簽名來保護————爲了安全性考慮,可能採用多種加密簽名方式來保護,這個只是爲了增長Signing Block數據的安全性而已;) 從上面的步驟能夠看到,這實際上就是數字簽名的實現方式; 計算分塊的策略以下,先將信息分紅1MB的連續塊,而後分別計算每塊的摘要,能夠經過並行處理加快計算速度;而後將獲得的分塊摘要再按照規則計算獲得最終的頂級摘要;

三、APK安裝驗證流程:

Google官方驗證APK流程圖:

驗證v2簽名的流程:
複製代碼

一、先找到APK Signing Block,代碼以下:

ApkSignatureSchemeV2Verifier.java
public static X509Certificate[][] verify(String apkFile)
            throws SignatureNotFoundException, SecurityException, IOException {
        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
            return verify(apk);
        }
    }
    
private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException {
    ......
    long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
        Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
        long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
    .....
}
複製代碼

二、對於v2分塊的每一個signer作驗證,首先找到此signer所採用的加密算法,而後對signed data作解密,確保獲得了正確的摘要信息,代碼以下:

private static X509Certificate[][] verify(
            FileDescriptor apkFileDescriptor,
            SignatureInfo signatureInfo) throws SecurityException {
    ......
    while (signers.hasRemaining()) {
            signerCount++;
            try {
                ByteBuffer signer = getLengthPrefixedSlice(signers);
                X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
                signerCerts.add(certs);
            } catch (IOException | BufferUnderflowException | SecurityException e) {
                throw new SecurityException(
                        "Failed to parse/verify signer #" + signerCount + " block",
                        e);
            }
        }
    ......
}
複製代碼

三、而後驗證最終的摘要信息是否正確,只要頂級摘要是正確的,代表摘要信息就是沒有被篡改的,代碼以下:

private static void verifyIntegrity(
            Map<Integer, byte[]> expectedDigests,
            FileDescriptor apkFileDescriptor,
            long apkSigningBlockOffset,
            long centralDirOffset,
            long eocdOffset,
            ByteBuffer eocdBuf) throws SecurityException {
    ......
            try {
            actualDigests =
                    computeContentDigests(
                            digestAlgorithms,
                            new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
        } catch (DigestException e) {
            throw new SecurityException("Failed to compute digest(s) of contents", e);
        }
        for (int i = 0; i < digestAlgorithms.length; i++) {
            int digestAlgorithm = digestAlgorithms[i];
            byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
            byte[] actualDigest = actualDigests[i];
            if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
                throw new SecurityException(
                        getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
                                + " digest of contents did not verify");
            }
        }
}

private static byte[][] computeContentDigests(
            int[] digestAlgorithms,
            DataSource[] contents) throws DigestException {
    ......
    for (DataSource input : contents) {
            long inputOffset = 0;
            long inputRemaining = input.size();
            while (inputRemaining > 0) {
                int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
                for (int i = 0; i < mds.length; i++) {
                    mds[i].update(chunkContentPrefix);
                }
        ......
    }
    
    for (int i = 0; i < digestAlgorithms.length; i++) {
            int digestAlgorithm = digestAlgorithms[i];
            byte[] input = digestsOfChunks[i];
            String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
            MessageDigest md;
            try {
                md = MessageDigest.getInstance(jcaAlgorithmName);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
            }
            byte[] output = md.digest(input);
            result[i] = output;
    }
    ......    
}

複製代碼
相關文章
相關標籤/搜索