Signature V2 search and verification of Android APK files

  First look at the official website's introduction to V2 signatures :
  APK Signature Scheme v2 is a full-file signature scheme that can discover all changes made to the protected part of the APK, thereby helping to speed up verification and enhance integrity assurance.
  When signing with APK Signature Scheme v2, an APK Signature block is inserted into the APK file immediately before and immediately before the "ZIP Central Directory" section. Within the APK Signature Block, the v2 signature and signer identity information is stored in the APK Signature Scheme v2 block.
signbeforeandafter

Figure 1. APK before and after signing

  APK Signature Scheme v2 was introduced in Android 7.0 (Nougat). In order for an APK to be installable on Android 6.0 (Marshmallow) and lower devices, the APK should be signed using the JAR signing feature before signing it using the v2 scheme.

APK Signature Block
  To maintain backward compatibility with the v1 APK format, APK signatures for v2 and higher are stored in the "APK Signature Block", a new container introduced to support APK Signature Scheme v2. In the APK file, the "APK Signature Block" is located immediately before and immediately before the "ZIP Central Directory" section at the end of the file.
  This chunk contains multiple ID-value pairs, packaged in a way that makes it easier to find the chunk in the APK. The APK's v2 signature is stored as an ID-value pair, where the ID is 0x7109871a.

Format
  The format of the "APK Signature Block" is as follows (all numeric fields are little-endian):

  • size of block, in bytes (excluding this field) (uint64)
  • Sequence of ID-value pairs prefixed with uint64 length:
    • ID (uint32)
    • value (variable length: length of "ID-value" pair - 4 bytes)
  • size of block, in bytes - same as first field (uint64)
  • magic "APK Signature Block 42" (16 bytes)

  When parsing an APK, the start of the "ZIP Central Directory" is first found by finding the "End of ZIP Central Directory" record at the end of the file, and then reading the starting offset of the "Central Directory" from that record . Through the magic value, it can be quickly determined that the "APK signature block" may be ahead of the "central directory". Then, through the value of size of block, the starting position of the block in the file can be efficiently found.
  ID-value pairs with unknown IDs shall be ignored when interpreting this chunk.

APK Signature Scheme v2 Chunked
  APKs are signed by one or more signers/identities, each represented by a signing key. This information is stored as an "APK Signature Scheme v2 Chunk". For each signer, the following information is stored:

  • (signature algorithm, digest, signature) tuple. Digests are stored so that signature verification and APK content integrity checking can be decoupled.
  • An X.509 certificate chain representing the identity of the signer.
  • Additional properties in the form of key-value pairs.
      For each signer, the APK is verified using the supported signatures from the received list. Signatures with unknown signature algorithms are ignored. If multiple supported signatures are encountered, it is up to each implementation to choose which one to use. This makes it possible to introduce more secure signing methods in a backwards-compatible manner in the future. The recommended approach is to verify the highest security signature.

Format
  "APK Signature Scheme v2 Chunk" is stored in "APK Signature Chunk" with ID 0x7109871a.
  The format of the "APK Signature Scheme v2 Chunk" is as follows (all numeric values ​​are in little-endian byte order, and all length-prefixed fields use uint32 values ​​for length):

  • A length-prefixed signer (length-prefixed) sequence:
    • signed data with length prefix:
      • A sequence of digests (length-prefixed) with a length prefix:
        • signature algorithm ID (uint32)
        • (with length prefix) digest - see integrity-protected content
      • Sequence of X.509 certificates with length prefix:
        • X.509 certificate with length prefix (ASN.1 DER format)
      • Sequence of additional attributes (length-prefixed) with length prefix:
        • ID (uint32)
        • value (variable length: length of additional attribute - 4 bytes)
    • Length-prefixed signatures (length-prefixed) sequence:
      -signature algorithm ID (uint32)
      -signed data length-prefixed signature
    • public key with length prefix (SubjectPublicKeyInfo, ASN.1 DER form)

  The above is the introduction of the V2 signature on the official website, and the following follows the source code according to the above introduction, specifically look at the search of the signature block and the verification process of the signature.

Signature block lookup

The apk file is actually a zip file compression package, and the steps   to find the signature are also introduced above . The implementation of the code is in the findSignature(RandomAccessFile apk) of the ApkSignatureSchemeV2Verifier class, which is located in the platform\frameworks\base\core\java\android\util\apk directory:

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

  The parameter apk is the RandomAccessFile object of the apk file to be searched, APK_SIGNATURE_SCHEME_V2_BLOCK_ID is the above-mentioned "APK signature scheme v2 block" stored in the "APK signature block", and the value is 0x7109871a. First find the APK signature block, and then find the APK signature scheme v2 block through APK_SIGNATURE_SCHEME_V2_BLOCK_ID. Then get the specific value.
  Look at the ApkSigningBlockUtils.findSignature() method again:

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

  This method mainly does several things:
  1. Find the data at the end of the central directory and the offset in the apk file.
  2. Obtain the signature block data and its offset in the apk file through the central directory.
  3. Find the v2 block data through blockId.
  Finally, the relevant data is encapsulated into the SignatureInfo class and the result is returned.

Find the data at the end of the central directory and the offset in the apk file

  It is implemented by getEocd(RandomAccessFile apk), and getEocd() is implemented by the static method findZipEndOfCentralDirectoryRecord() of the ZipUtils class, take a look at it:

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

  It can be seen that it is mainly implemented by the findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize) method. We found that it may be called twice. The first second parameter is 0. If the result is null, the second parameter will be Set to UINT16_MAX_VALUE to continue the method.

  If you want to understand how it is queried, you also need to understand the data structure at the end of the central directory.

name illustrate
central catalog tail 4-byte fixed value (0x06054b50)
the disk number 2 bytes
The disk number where the central directory starts 2 bytes
Total number of entities in the central directory on this disk 2 bytes
Total number of entities in the central directory 2 bytes
central directory size 4 bytes
The offset of the starting location of the central directory from the starting disk number 4 bytes
ZIP file comment length 2 bytes
ZIP file comment Data of variable length

  The last part of the zip compressed file is the tail of the central directory. Its structure is a 22-byte fixed-length value and an unfixed-length comment value. This unfixed length is at most 2 bytes in length. Because about 99.99% of the apk files are 0-length comment values, it is an optimization strategy to use 0 directly to find the end of the central directory at the beginning. Read data from the 22 bytes before the end of the APK. If the first fixed value is 0x06054b50 and the corresponding comment length is 0, it is considered that the end of the central directory has been found.
  If the length is 0 and the end of the central directory is not found, it is considered that there is comment data, and the longest length UINT16_MAX_VALUE (65536) is used for query. Because the first 22 bytes of the comment are fixed-length data, read the data of UINT16_MAX_VALUE + 22 bytes before the end of the APK, and check from the last 22 bytes of the data. If you find the first fixed value 0x06054b50, then start from The length of the comment obtained in the data is consistent with the length of the comment obtained by the actual count, and it is considered that the end of the central directory has been found.
  The overall query idea is the logic described above.
  Here I also want to talk about the result Pair<ByteBuffer, Long> returned by findZipEndOfCentralDirectoryRecord(). The second part of the Pair object is the position offset of the end of the central directory in the APK file. The first is the HeapByteBuffer object, which is allocated on the heap. We know above that we look for the data read at the end of the central directory, and if there is a comment, it will read UINT16_MAX_VALUE + 22 bytes of data. But the annotation is not necessarily UINT16_MAX_VALUE length, it may be less than it. So how does it identify the starting position of the data in the HeapByteBuffer object from the end of the central directory? ByteBuffer has a member variable offset, which is used to identify the starting position.

Get the signature block data and the offset in the apk file through the central directory

  After getting the data at the end of the central directory, the location and size of the central directory can be obtained. Also look at the data structure content at the end of the central directory , where the offset of the starting position of the central directory compared to the starting disk number, and the central directory size sub-table identify the location and size of the central directory.
  getCentralDirOffset(eocd, eocdOffset) obtains the offset of the central directory through the values ​​of these two fields.
  Then findApkSigningBlock(apk, centralDirOffset) gets the data and offset of the signature block through the location of the central directory. We know that the data of the signature block is before the central directory, and through the data format
  of the block , the data size of the signature block can be known at 24 bytes before the central directory. This way we can find the offset of the signature block. findApkSigningBlock(apk, centralDirOffset) is found according to this logic.

Find the data of the v2 block by blockId

  The data of the v2 block is found by findApkSignatureSchemeBlock(apkSigningBlock, blockId), where the blockId is also said to be 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.");
    }

  Because the 8th byte of the signature block is the specific entity data, and it ends at the penultimate 24th byte of the block data. So start by adjusting the ByteBuffer parameters through sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24). Take a look, the specific implementation:

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

  In fact, a new ByteBuffer object is regenerated through the slice() method of the ByteBuffer object, and its related parameters are adjusted. But the newly generated ByteBuffer object and the original ByteBuffer object use the same array.
  Here, first set the limit of the source to the penultimate 24th byte position of the block data, position to the 8th byte position of the block, and then execute the slice() method. In this ByteBuffer object is actually a HeapByteBuffer object, so call is the slice() method of the HeapByteBuffer object.

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

  remaining() is limit-position, so the mark of the newly generated ByteBuffer object is -1, position is 0, limit is remaining(), capacity is remaining(), and offset is position() + offset, position() is currently It is the 8th byte position of the block, and the offset is 0.
  What to do with setting the offset of the ByteBuffer object is to add the data corresponding to the position after the offset value from the current position in order to read the data later.
  After findApkSignatureSchemeBlock() adjusts the ByteBuffer object, it is time to find the v2 block data. Signature block data contains not only v2 block data, but may also contain other entity data. Each of these entities begins with the length of the entity. In this way, we can quickly find the next entity through this length.
  So findApkSignatureSchemeBlock() uses a loop to get the length of the entity first. The data next to this length is the ID value, so that by looping through the entity and comparing the ID values, the v2 block data can be found. The v2 block data is obtained through getByteBuffer(pairs, len - 4), which also obtains a new ByteBuffer object through the slice() method of the HeapByteBuffer object.

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

  It can be seen that the available data size of the newly generated ByteBuffer object here is the parameter size. And slice() will reset the offset of the ByteBuffer object according to the sum of position and offset. After generating a new object, pay attention to setting the original ByteBuffer object member variable position to limit, indicating that the original ByteBuffer object will skip the data of the newly generated ByteBuffer object and restore the original ByteBuffer object limit.

  In this way, the SignatureInfo object is obtained through findSignature(RandomAccessFile apk) of the ApkSignatureSchemeV2Verifier class, which includes the signature v2 block data, the offset position of the signature block, the offset position of the central directory, the offset position of the tail of the central directory, and the central directory tail data.

Signature Verification Process

  The verification process is implemented in verify() of the ApkSignatureSchemeV2Verifier class, the code is as follows:

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

  For the format of the V2 block data, refer to the above v2 block format . As mentioned above, signatureInfo.signatureBlock is the v2 block data. At the beginning, it is an int length, which is the length of all subsequent signers. getLengthPrefixedSlice(signatureInfo.signatureBlock) is to get a new ByteBuffer object signers by reading the first int length.
  Then start a loop to read and process all signers. signer is the signature information. As you can see here, an APK can allow multiple signatures.
  verity mainly does the following things:
  1. Call verifySigner(signer, contentDigests, certFactory) to verify the signature
  2. According to the parameter doVerifyIntegrity, decide whether to perform file integrity verification, and call ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo) Method, its details are in the next article Android APK file integrity verification
  3. If the v2 block includes the CONTENT_DIGEST_VERITY_CHUNKED_SHA256 algorithm, call ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength() to get the Merkle tree root digest.
  Finally, it is encapsulated into a VerifiedSigner object and returned. It contains the certificates of each signer, the Merkle root digest, and the best digest algorithm for each signer.
  Let's mainly look at the first step of verifying the signature

verify signature

  The verifySigner(signer, contentDigests, certFactory) method is a bit long, read it in sections, the first paragraph:

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

  Call getLengthPrefixedSlice() to get signed data and signatures data respectively. Get the public key data through readLengthPrefixedByteArray().
  First pass through the loop to process the signatures data. When parsing, the algorithm will be put into signaturesSigAlgorithms. If there are multiple signature algorithms, the best signature algorithm will be selected by comparison, and the signature bestSigAlgorithmSignatureBytes obtained after the signed data passes the signature algorithm is obtained.
  Take a look at the supported algorithms:

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

  If the best signature algorithm is not found, a SecurityException will be reported.
  verifySigner() second piece of code:

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

  First get the JCA algorithm corresponding to the best algorithm, and then get the JCA signature algorithm and parameters corresponding to the best algorithm. Then get the public key, and then verify the signed data and signature bestSigAlgorithmSignatureBytes through the public key. If the verification fails, a SecurityException will be reported.
  verifySigner() third piece of code:

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

  This block deals with the digests sequence in signedData, and its data structure refers to the v2 block format . Get the summary algorithm, put the summary algorithm into digestsSigAlgorithms. And if it is the same as the previous best algorithm bestSigAlgorithm, the summary data will be read into contentDigest.
  After reading, digestsSigAlgorithms and signaturesSigAlgorithms will be compared. If different, an exception will be reported.
  Then use the getSignatureAlgorithmContentDigestAlgorithm() method to convert the signature algorithm into the corresponding digest algorithm. Also put the digest algorithm and digest data into the parameter contentDigests. If the digest algorithm has been used in the previous signature, and the previous digest is different from the current digest, a SecurityException will be thrown.
  The last piece of code in 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()]);
    }

  At this time, in order to process the X.509 certificates sequence in signedData, the data structure refers to the v2 block format . The integer data will be encapsulated into a VerbatimX509Certificate object and put into certs. If there is no certificate, a SecurityException will be reported.
  And if the public key of the first certificate in certs is different from the public key data publicKeyBytes read out earlier, a SecurityException will also be reported.
  Next, the additional attributes sequence in signedData will be processed, and the structure of the data refers to the v2 block format . Call verifyAdditionalAttributes(ByteBuffer attrs) for processing.
  The final result returned is an array of certificates.

Summarize

  This article mainly reorganizes the search and verification process of V2 signatures based on official documents and combined with source code.
  In this process, the read data is stored using the HeapByteBuffer class, so it is necessary to understand the use of the HeapByteBuffer class, especially the use of slice().
  When looking for the V2 signature block, we need to understand the structure of the APK file, understand that the signature data block is stored before the central directory, and need to know the data structure of the signature block.
  In the signature verification process, it is necessary to be familiar with the data storage structure of the v2 block. By finding the data that needs to be signed, the signature data, as well as the signature algorithm and public key. After that, the data signed by the private key can be verified using the public key.

Guess you like

Origin blog.csdn.net/q1165328963/article/details/132521068