关于安卓签名策略的一些理解

1,401 阅读14分钟

标签(空格分隔): 安卓签名


#一、安卓生成APK安装包

1、安卓打包过程

安卓打包过程可参考google给出的APK打包流程图,

流程图
最终通过apkbuilder生成的apk实际上最终的存储就是一个zip压缩包,因此可以参考zip压缩包的存储格式来理解apk的存储,当然apk打包前已经做了二进制处理、资源压缩、dex转换等操作。

2、ZipAlign

经过aapt编译生成的APK,实际上是一个有内部文件规范的zip压缩包;可以通过使用ZipAlign命令确保所有未压缩的数据的开头均相对于文件开头部分执行特定的字节对齐,这样可减少应用消耗的 RAM 量;但是由于需要对数据采用边界对齐,apk包的体积会增大,大约增加了100KB左右;

二、jarsigner签名工具

1、v1签名方案

jarSigner签名方式由JDK提供,jarSigner签名后生成一个META-INF文件夹。 1、MANIFEST.MF文件,这个文件包含了APK压缩后的所有文件对应的摘要信息,每个文件路径和对应的摘要信息都列举出来:

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

Name: res/drawable/upgrade_progress.xml
SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho=

2、CERT.SF文件,SF则是MF文件的摘要信息以及.MF文件当中每个条目在用摘要算法计算得到的摘要信息并用base64编码保存; 3、CERT.RSA,CERT.SF文件则存放证书信息,公钥信息,以及用私钥对.SF文件的加密数据即签名信息;

2、APK安装安卓验证jarsigner签名

使用jarsigner签名的APK安装时候,验证可以参考sdk/sources/$sdkversion/android/util/jar下面的文件,验证主要包括两个部分,第一步通过CERT.RSA文件验证CERT.SF文件,参考方法:

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目录下的文件没有被篡改:

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文件对应的文件摘要信息,类似:

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

Name: res/drawable/upgrade_progress.xml
SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho=

然后对应确认每一个文件对应的摘要信息是否是正确的,以此来确保APK解压后的文件都没有被修改过;

3、jarsigner签名的缺陷

根据上面安卓校验jarsigner的过程,可以看到jarsigner签名后的APK可能有如下问题: 1、对于META-INF文件夹,安卓只会校验CERT.SF三个文件,如若在META-INF存放其他文件,会逃过安卓的检测过程,此处存在较大的安全漏洞;(参考美团通过在META-INF下添加一个空文件来代表渠道号) 2、每次安装APK都需要通过解压APK再做对应的校验,解压APK是一个耗时耗电的过程,安装过程体验不好;

二、APK Signature Scheme v2签名

1、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的优先两个:1、签名更快,安装时候验证签名也更快,直接对二进制文件操作,而不需要像jarsigner那样需要先压缩文件签名,先解压文件验证签名,效率太低;2、安全性更好,用户想要抹掉签名重新修改文件的成本更高,需要对整个ApkSigner原理非常清楚,二次签名的成本更高; 但是如上面所述,ApkSigner并不能防止二次签名,要防二次签名需要有其他方案;

v2分块的本质就是数字签名的过程,因此会存储对应的加解密信息和摘要信息; 每一个APK签名方案v2分块对应一个签名者/身份签名,有多个签名者则含有多个v2分块;v2分块结构信息如下: v2分块是用来保护APK全文件的,明文即APK全文件信息,v2分块存储了摘要算法和摘要信息,同时存储了数字证书信息和加密算法信息,并提供公钥信息;类似数字签名的校验一致,最终通过计算就能够证明APK已经做了v2签名,且apk内容没有被篡改; 目前APK Signature算法支持的主流的摘要算法,同时支持RSA、DSA、EC椭圆加密等非对称算法; APK Signature算法实质是通过对APK全内容做类似数字签名的工作,来保证APK文件不会被篡改。

2、APK Signature保护APK内容的实现

首先打包的APK转换成zip其文件结构如下: ?问题1:可以把APK当作Zip文件来处理,但是Zip结构是有几个限制条件的,比如zipcommentfield对应到什么内容,zip eocd会有 comment field?

APK Signature算法主要做了两个事情: a,计算第1、3、4部分内容的摘要,将这些摘要信息存储到APK Signing Block的v2分块的signed data分块;(?是对最终的顶级摘要加密签名还是对每个分块摘要都加密签名————最终计算得到的顶级摘要信息存储到singed data分块,因为最终只是按照规则计算最终摘要相同即可;) b,将上面得到的分块(摘要信息)通过一个或多个加密算法来加密;(?对顶级摘要可能采用一个或多个签名来保护————为了安全性考虑,可能采用多种加密签名方式来保护,这个只是为了增加Signing Block数据的安全性而已;) 从上面的步骤可以看到,这实际上就是数字签名的实现方式; 计算分块的策略如下,先将信息分成1MB的连续块,然后分别计算每块的摘要,可以通过并行处理加快计算速度;然后将得到的分块摘要再按照规则计算得到最终的顶级摘要;

3、APK安装验证流程:

Google官方验证APK流程图:

验证v2签名的流程:

1、先找到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);
    .....
}

2、对于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);
            }
        }
    ......
}

3、然后验证最终的摘要信息是否正确,只要顶级摘要是正确的,表明摘要信息就是没有被篡改的,代码如下:

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;
    }
    ......    
}