Android APK文件的签名V2查找、验证

  先看一下官网对V2签名的介绍:
  APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。
  使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。
Android APK文件的签名V2查找、验证_第1张图片

图 1. 签名前和签名后的 APK

  APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。

APK 签名分块
  为了保持与 v1 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”(位于文件末尾)之前并紧邻该部分。
  该分块包含多个“ID-值”对,所采用的封装方式有助于更轻松地在 APK 中找到该分块。APK 的 v2 签名会存储为一个“ID-值”对,其中 ID 为 0x7109871a。

格式
  “APK 签名分块”的格式如下(所有数字字段均采用小端字节序):

  • size of block,以字节数(不含此字段)计 (uint64)
  • 带 uint64 长度前缀的“ID-值”对序列:
    • ID (uint32)
    • value(可变长度:“ID-值”对的长度 - 4 个字节)
  • size of block,以字节数计 - 与第一个字段相同 (uint64)
  • magic“APK 签名分块 42”(16 个字节)

  在解析 APK 时,首先要通过以下方法找到“ZIP 中央目录”的起始位置:在文件末尾找到“ZIP 中央目录结尾”记录,然后从该记录中读取“中央目录”的起始偏移量。通过 magic 值,可以快速确定“中央目录”前方可能是“APK 签名分块”。然后,通过 size of block 值,可以高效地找到该分块在文件中的起始位置。
  在解译该分块时,应忽略 ID 未知的“ID-值”对。

APK 签名方案 v2 分块
  APK 由一个或多个签名者/身份签名,每个签名者/身份均由一个签名密钥来表示。该信息会以“APK 签名方案 v2 分块”的形式存储。对于每个签名者,都会存储以下信息:

  • (签名算法、摘要、签名)元组。摘要会存储起来,以便将签名验证和 APK 内容完整性检查拆开进行。
  • 表示签名者身份的 X.509 证书链。
  • 采用键值对形式的其他属性。
      对于每位签名者,都会使用收到的列表中支持的签名来验证 APK。签名算法未知的签名会被忽略。如果遇到多个支持的签名,则由每个实现来选择使用哪个签名。这样一来,以后便能够以向后兼容的方式引入安全系数更高的签名方法。建议的方法是验证安全系数最高的签名。

格式
  “APK 签名方案 v2 分块”存储在“APK 签名分块”内,ID 为 0x7109871a。
  “APK 签名方案 v2 分块”的格式如下(所有数字值均采用小端字节序,所有带长度前缀的字段均使用 uint32 值表示长度):

  • 带长度前缀的 signer(带长度前缀)序列:
    • 带长度前缀的 signed data:
      • 带长度前缀的 digests(带长度前缀)序列:
        • signature algorithm ID (uint32)
        • (带长度前缀)digest - 请参阅受完整性保护的内容
      • 带长度前缀的 X.509 certificates 序列:
        • 带长度前缀的 X.509 certificate(ASN.1 DER 格式)
      • 带长度前缀的 additional attributes(带长度前缀)序列:
        • ID (uint32)
        • value(可变长度:附加属性的长度 - 4 个字节)
    • 带长度前缀的 signatures(带长度前缀)序列:
      -signature algorithm ID (uint32)
      -signed data 上带长度前缀的 signature
    • 带长度前缀的 public key(SubjectPublicKeyInfo,ASN.1 DER 形式)

  以上就是官网上对V2签名的介绍,下面跟着源代码按照上面介绍的,具体的看一下,签名块的查找、签名的验证过程。

签名块的查找

  apk文件其实就是zip文件压缩包,上面也介绍了查找签名的步骤。代码的实现是在ApkSignatureSchemeV2Verifier类的findSignature(RandomAccessFile apk)中,它位于platform\frameworks\base\core\java\android\util\apk目录下:

    public static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
        return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
    }

  参数apk就是要查找的apk文件的RandomAccessFile 对象,APK_SIGNATURE_SCHEME_V2_BLOCK_ID就是上面说的 “APK 签名方案 v2 分块”存储在“APK 签名分块”内,值为 0x7109871a。先找到APK 签名分块,然后通过APK_SIGNATURE_SCHEME_V2_BLOCK_ID找到APK 签名方案 v2 分块。然后再得到具体的值。
  再看下ApkSigningBlockUtils.findSignature()方法:

    static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
            throws IOException, SignatureNotFoundException {
        // Find the ZIP End of Central Directory (EoCD) record.
        Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
        ByteBuffer eocd = eocdAndOffsetInFile.first;
        long eocdOffset = eocdAndOffsetInFile.second;
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new SignatureNotFoundException("ZIP64 APK not supported");
        }

        // Find the APK Signing Block. The block immediately precedes the Central Directory.
        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 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock,
                blockId);

        return new SignatureInfo(
                apkSignatureSchemeBlock,
                apkSigningBlockOffset,
                centralDirOffset,
                eocdOffset,
                eocd);
    }

  该方法主要做了一下几件事:
  一、找到中央目录尾部的数据和在apk文件中的偏移量。
  二、通过中央目录得到签名分块数据和其在apk文件中的偏移量。
  三、通过blockId找到v2 分块的数据。
  最后将相关数据封装到SignatureInfo类中返回结果。

找到中央目录尾部的数据和在apk文件中的偏移量

  它是由getEocd(RandomAccessFile apk)实现的,而getEocd()是由ZipUtils类的静态方法findZipEndOfCentralDirectoryRecord()实现,看一下它:

    static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
            throws IOException {
        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
        // The record can be identified by its 4-byte signature/magic which is located at the very
        // beginning of the record. A complication is that the record is variable-length because of
        // the comment field.
        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
        // the candidate record's comment length is such that the remainder of the record takes up
        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.

        long fileSize = zip.length();
        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
            return null;
        }

        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
        // reading more data.
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
        if (result != null) {
            return result;
        }

        // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
        // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
        // the comment length field is an unsigned 16-bit number.
        return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
    }

  可见它主要是由findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize)方法实现的,我们发现它有可能会调用两次,第一次第二个参数传递的是0,如果结果为null,会将第二个参数设置为UINT16_MAX_VALUE继续执行该方法。

  如果想要明白它是怎么查询的,还需要了解一下中央目录尾部的数据结构。

名称 说明
中央目录尾部标识 4字节 固定值(0x06054b50)
该磁盘编号 2字节
中央目录开始位置的磁盘编号 2字节
该磁盘上的中央目录中的实体总数 2字节
中央目录中实体总数 2字节
中央目录大小 4字节
中央目录开始位置相较于起始磁盘号的偏移量 4字节
ZIP文件注释长度 2字节
ZIP文件注释 不固定长度的数据

  zip压缩文件的最后部分是中央目录尾部,它的结构是22字节固定长度值和不固定长度的注释值,这个不固定长度最多是2个字节长度。因为apk文件大概99.99%都是0长度的注释值,所以刚开始就直接使用0来查找中央目录尾部,这是一种优化策略。从APK的结尾前22字节开始读取数据,如果得到第一个固定值为0x06054b50,并且找到对应的注释长度为0,则认为找到中央目录尾部。
  如果长度为0,没查到中央目录尾部,则认为存在注释数据,则使用最长的长度UINT16_MAX_VALUE(65536)进行查询。因为注释前22字节是固定长度数据,将APK结尾前UINT16_MAX_VALUE + 22字节的数据读取下来,从数据的后22字节开始往前查,如果找到第一个固定值为0x06054b50,然后从数据中取到注释的长度和实际计数得到的注释长度一致,认为找到中央目录结尾。
  总体的查询思想就是上面介绍的这样的逻辑。
  这里还要说一下findZipEndOfCentralDirectoryRecord()返回的结果Pair,Pair对象的第二个就是中央目录结尾在APK文件中的位置偏移量。第一个是HeapByteBuffer对象,它是分配在堆上面的。上面我们知道,我们查找中央目录尾部读取的数据,如果存在注释时,会读取UINT16_MAX_VALUE + 22字节的数据。但是注释不见得是UINT16_MAX_VALUE长度,可能小于它。那么它是怎么识别HeapByteBuffer对象中的数据从中央目录尾部标识开始的位置的呢,ByteBuffer有一个成员变量offset,就是通过它来标识开始位置的。

通过中央目录得到签名分块数据和在apk文件中的偏移量

  得到了中央目录尾部的数据,就能得到中央目录的位置和大小。还看中央目录尾部的数据结构内容,其中字段 中央目录开始位置相较于起始磁盘号的偏移量、中央目录大小 分表标识中央目录的位置和大小。
  getCentralDirOffset(eocd, eocdOffset)就是通过这两个字段的值得到了中央目录的偏移量。
  紧接着findApkSigningBlock(apk, centralDirOffset)就通过中央目录的位置得到了签名分块的数据和偏移位置。
  我们知道签名分块的数据是在中央目录之前,并且通过分块的数据格式知道,在中央目录之前24字节处能知道签名分块的数据大小。这样我们就能找到签名分块的偏移位置。findApkSigningBlock(apk, centralDirOffset)就是按照这个逻辑查找的。

通过blockId找到v2 分块的数据

  v2分块的数据是通过findApkSignatureSchemeBlock(apkSigningBlock, blockId)找到的,这里blockId前面也说了是0x7109871a。

    static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
            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);

        int entryCount = 0;
        while (pairs.hasRemaining()) {
            entryCount++;
            if (pairs.remaining() < 8) {
                throw new SignatureNotFoundException(
                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
            }
            long lenLong = pairs.getLong();
            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount
                                + " size out of range: " + lenLong);
            }
            int len = (int) lenLong;
            int nextEntryPos = pairs.position() + len;
            if (len > pairs.remaining()) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
                                + ", available: " + pairs.remaining());
            }
            int id = pairs.getInt();
            if (id == blockId) {
                return getByteBuffer(pairs, len - 4);
            }
            pairs.position(nextEntryPos);
        }

        throw new SignatureNotFoundException(
                "No block with ID " + blockId + " in APK Signing Block.");
    }

  因为签名分块的第8个字节开始才是具体的实体数据,并且到分块数据的倒数第24个字节结束。所以开始就通过sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24)调整ByteBuffer 的参数。看一下,具体的实现:

    static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
        if (start < 0) {
            throw new IllegalArgumentException("start: " + start);
        }
        if (end < start) {
            throw new IllegalArgumentException("end < start: " + end + " < " + start);
        }
        int capacity = source.capacity();
        if (end > source.capacity()) {
            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
        }
        int originalLimit = source.limit();
        int originalPosition = source.position();
        try {
            source.position(0);
            source.limit(end);
            source.position(start);
            ByteBuffer result = source.slice();
            result.order(source.order());
            return result;
        } finally {
            source.position(0);
            source.limit(originalLimit);
            source.position(originalPosition);
        }
    }

  其实就是通过 ByteBuffer对象的slice()方法重新生成一个新的ByteBuffer对象,调整了一下它的相关参数。但是新生成的ByteBuffer对象和原来的ByteBuffer对象使用的是同一个数组。
  这里先设置source的limit为分块数据的倒数第24个字节位置、position为分块的第8个字节位置,然后执行了slice()方法,在这ByteBuffer对象实际是HeapByteBuffer对象,所以调用的是HeapByteBuffer对象的slice()方法。

    public ByteBuffer slice() {
        return new HeapByteBuffer(hb,
                -1,
                0,
                remaining(),
                remaining(),
                position() + offset,
                isReadOnly);
    }

  remaining()是limit-position,所以新生成的ByteBuffer对象的mark为-1,position为0,limit为remaining(),capacity为remaining(),还有offset为position() + offset,position()目前是分块的第8个字节位置,offset为0。
  设置ByteBuffer对象的offset做啥,其实就是为了以后读取数据,都要从当前位置加上这个偏移值之后位置对应的数据。
  findApkSignatureSchemeBlock()调整好ByteBuffer对象之后,接着就要去找v2分块数据了。签名分块数据不止包含v2分块数据,可能还包含其他实体数据。这每一个实体的开头都是这个实体的长度。这样,我们通过这个长度能很快找到下一个实体。
  所以findApkSignatureSchemeBlock()就使用了一个循环,先得到实体的长度。紧挨着这个长度的数据就是ID值,这样通过循环实体,然后比较ID值,就能找到v2分块数据。得到v2分块数据是通过getByteBuffer(pairs, len - 4)得到的,其中里面也是通过HeapByteBuffer对象的slice()方法来得到一个新的ByteBuffer对象。

    static ByteBuffer getByteBuffer(ByteBuffer source, int size)
            throws BufferUnderflowException {
        if (size < 0) {
            throw new IllegalArgumentException("size: " + size);
        }
        int originalLimit = source.limit();
        int position = source.position();
        int limit = position + size;
        if ((limit < position) || (limit > originalLimit)) {
            throw new BufferUnderflowException();
        }
        source.limit(limit);
        try {
            ByteBuffer result = source.slice();
            result.order(source.order());
            source.position(limit);
            return result;
        } finally {
            source.limit(originalLimit);
        }
    }

  可见这里新生成的ByteBuffer对象的可用数据大小为参数size。并且slice()会根据position和offset的和重新设置ByteBuffer对象的偏移量。生成新的对象之后,注意会把原来的ByteBuffer 对象成员变量position设置为limit,说明原来的ByteBuffer 对象会跳过新生成的ByteBuffer对象的数据,将原来的ByteBuffer 对象limit还原。

  这样就通过ApkSignatureSchemeV2Verifier类的findSignature(RandomAccessFile apk)得到了SignatureInfo对象,它里面包括签名v2分块数据、签名分块的偏移位置、中央目录的偏移位置、中央目录尾部的偏移位置、中央目录尾部的数据。

签名的验证过程

  验证过程实现在ApkSignatureSchemeV2Verifier类的verify()中,代码如下:

    private static VerifiedSigner verify(
            RandomAccessFile apk,
            SignatureInfo signatureInfo,
            boolean doVerifyIntegrity) throws SecurityException, IOException {
        int signerCount = 0;
        Map<Integer, byte[]> contentDigests = new ArrayMap<>();
        List<X509Certificate[]> signerCerts = new ArrayList<>();
        CertificateFactory certFactory;
        try {
            certFactory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
        }
        ByteBuffer signers;
        try {
            signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
        } catch (IOException e) {
            throw new SecurityException("Failed to read list of signers", e);
        }
        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);
            }
        }

        if (signerCount < 1) {
            throw new SecurityException("No signers found");
        }

        if (contentDigests.isEmpty()) {
            throw new SecurityException("No content digests found");
        }

        if (doVerifyIntegrity) {
            ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
        }
        byte[] verityRootHash = null;
        if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
            byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
            verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
                    verityDigest, apk.length(), signatureInfo);
        }

        return new VerifiedSigner(
                signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
                verityRootHash, contentDigests);
    }

  V2分块数据的格式参考上面v2分块格式,signatureInfo.signatureBlock前面说了,它就是v2分块数据。刚开始就是一个int长度,它是后面所有signer的长度。getLengthPrefixedSlice(signatureInfo.signatureBlock)就是通过读取首个int长度来得到一个新的ByteBuffer对象signers。
  接着开启一个循环,来读取处理所有signer。signer就是签名信息。在这里可以看到,一个APK能允许多个签名。
  verity主要做了以下几件事:
  一、调用verifySigner(signer, contentDigests, certFactory)验证签名
  二、根据参数doVerifyIntegrity,来决定是否做文件完整性验证,调用的是ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo)方法,它的详细在下一篇文章Android APK文件完整性验证
  三、如果v2分块中包括CONTENT_DIGEST_VERITY_CHUNKED_SHA256算法,调用ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength()得到Merkle树根摘要。
  最后封装成VerifiedSigner对象返回。它包含各个签名者的证书,Merkle树根摘要,还有每个签名者中最好的摘要算法。
  下面主要看一下第一步的验证签名

验证签名

  verifySigner(signer, contentDigests, certFactory)方法有些长,分段来阅读,第一段:

    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);

        int signatureCount = 0;
        int bestSigAlgorithm = -1;
        byte[] bestSigAlgorithmSignatureBytes = null;
        List<Integer> signaturesSigAlgorithms = new ArrayList<>();
        while (signatures.hasRemaining()) {
            signatureCount++;
            try {
                ByteBuffer signature = getLengthPrefixedSlice(signatures);
                if (signature.remaining() < 8) {
                    throw new SecurityException("Signature record too short");
                }
                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) {
                throw new SecurityException(
                        "Failed to parse signature record #" + signatureCount,
                        e);
            }
        }
        if (bestSigAlgorithm == -1) {
            if (signatureCount == 0) {
                throw new SecurityException("No signatures found");
            } else {
                throw new SecurityException("No supported signatures found");
            }
        }

  调用getLengthPrefixedSlice()分别得到signed data、signatures数据。通过readLengthPrefixedByteArray()得到public key数据。
  先通过循环,处理signatures数据。解析的时候,会将算法放到signaturesSigAlgorithms中。如果存在多个签名算法,会通过比较选出最好的签名算法,并且得到signed data通过签名算法之后得到的签名bestSigAlgorithmSignatureBytes 。
  看一下支持的算法:

    static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
        switch (sigAlgorithm) {
            case SIGNATURE_RSA_PSS_WITH_SHA256:
            case SIGNATURE_RSA_PSS_WITH_SHA512:
            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
            case SIGNATURE_ECDSA_WITH_SHA256:
            case SIGNATURE_ECDSA_WITH_SHA512:
            case SIGNATURE_DSA_WITH_SHA256:
            case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
            case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
            case SIGNATURE_VERITY_DSA_WITH_SHA256:
                return true;
            default:
                return false;
        }
    }

  如果没有找到最好的签名算法,会报SecurityException异常。
  verifySigner()第二段代码:

        String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
        Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
                getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
        String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
        AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
        boolean sigVerified;
        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");
        }

  先得到最好算法对应的JCA算法,再得到最好算法对应的JCA签名算法和参数。之后得到公钥,然后通过公钥验证signed data和签名bestSigAlgorithmSignatureBytes。如果验证没通过,会报SecurityException异常。
  verifySigner()第三段代码:

        byte[] contentDigest = null;
        signedData.clear();
        ByteBuffer digests = getLengthPrefixedSlice(signedData);
        List<Integer> digestsSigAlgorithms = new ArrayList<>();
        int digestCount = 0;
        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);
            }
        }

        if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
            throw new SecurityException(
                    "Signature algorithms don't match between digests and signatures records");
        }
        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");
        }

  这块处理的是signedData中的digests序列,其数据的结构参考v2分块格式。得到其中的摘要算法,将摘要算法都放入digestsSigAlgorithms中。并且和前面最好的算法bestSigAlgorithm如果相同,则会将摘要数据读入contentDigest中。
  读取完毕后,会比较digestsSigAlgorithms和signaturesSigAlgorithms。如果不同,则会报异常。
  接着通过getSignatureAlgorithmContentDigestAlgorithm()方法,将签名算法转化成对应的摘要算法。还将摘要算法和摘要数据放入参数contentDigests中。如果之前有签名已经使用了该摘要算法,并且之前的摘要和当前的摘要不同,则会抛出SecurityException异常。
  verifySigner()最后一段代码:

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

        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");
        }

        ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
        verifyAdditionalAttributes(additionalAttrs);

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

  这时为了处理signedData中的X.509 certificates序列,其数据的结构参考v2分块格式。会将整数数据封装成VerbatimX509Certificate对象,并且放入certs中。如果不存在证书,会报SecurityException异常。
  并且certs中第一个证书的公钥和前面读出来的公钥数据publicKeyBytes不同,也会报SecurityException异常。
  接着会处理signedData中的additional attributes序列,其数据的结构参考v2分块格式。调用verifyAdditionalAttributes(ByteBuffer attrs)来进行处理。
  最后返回的结果是证书数组。

总结

  本篇文章主要根据官方文档,再结合源代码,重新梳理了一遍V2签名的查找、验证过程。
  在这个过程中,读取出来的数据,都是使用HeapByteBuffer类实现存储的,所以需要好好理解一下HeapByteBuffer类的使用,尤其是slice()使用。
  在查找V2签名块时,我们需要了解APK文件的结构,明白签名数据块在中央目录之前存放,并且需要知道签名块的数据结构。
  在签名验证过程中,更需要熟悉v2分块的数据存储结构形式。通过找到需要签名的数据,签名数据,还有签名算法、公钥。之后,就能使用公钥验签私钥签名的数据。

你可能感兴趣的:(android,java)