godot/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java

/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.apksig.internal.apk.v3;

import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;

import com.android.apksig.ApkVerificationIssue;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;

import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * APK Signature Scheme v3 verifier.
 *
 * <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
 * single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
 * uncompressed contents of ZIP entries.
 *
 * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
 */
public class V3SchemeVerifier {
    private final RunnablesExecutor mExecutor;
    private final DataSource mApk;
    private final ApkUtils.ZipSections mZipSections;
    private final ApkSigningBlockUtils.Result mResult;
    private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
    private final int mMinSdkVersion;
    private final int mMaxSdkVersion;
    private final int mBlockId;
    private final OptionalInt mOptionalRotationMinSdkVersion;
    private final boolean mFullVerification;

    private ByteBuffer mApkSignatureSchemeV3Block;

    private V3SchemeVerifier(
            RunnablesExecutor executor,
            DataSource apk,
            ApkUtils.ZipSections zipSections,
            Set<ContentDigestAlgorithm> contentDigestsToVerify,
            ApkSigningBlockUtils.Result result,
            int minSdkVersion,
            int maxSdkVersion,
            int blockId,
            OptionalInt optionalRotationMinSdkVersion,
            boolean fullVerification) {
        mExecutor = executor;
        mApk = apk;
        mZipSections = zipSections;
        mContentDigestsToVerify = contentDigestsToVerify;
        mResult = result;
        mMinSdkVersion = minSdkVersion;
        mMaxSdkVersion = maxSdkVersion;
        mBlockId = blockId;
        mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
        mFullVerification = fullVerification;
    }

    /**
     * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
     * verification. The APK must be considered verified only if
     * {@link ApkSigningBlockUtils.Result#verified} is
     * {@code true}. If verification fails, the result will contain errors -- see
     * {@link ApkSigningBlockUtils.Result#getErrors()}.
     *
     * <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
     * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
     * If the APK's signature is expected to not verify on any of the specified platform versions,
     * this method returns a result with one or more errors and whose
     * {@code Result.verified == false}, or this method throws an exception.
     *
     * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
     * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
     * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
     *
     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
     *         required cryptographic algorithm implementation is missing
     * @throws SignatureNotFoundException if no APK Signature Scheme v3
     * signatures are found
     * @throws IOException if an I/O error occurs when reading the APK
     */
    public static ApkSigningBlockUtils.Result verify(
            RunnablesExecutor executor,
            DataSource apk,
            ApkUtils.ZipSections zipSections,
            int minSdkVersion,
            int maxSdkVersion)
            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
        return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
                .setRunnablesExecutor(executor)
                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
                .build()
                .verify();
    }

    /**
     * Verifies the provided APK's v3 signatures and outputs the results into the provided
     * {@code result}. APK is considered verified only if there are no errors reported in the
     * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
     * int)} for more information about the contract of this method.
     *
     * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
     *        APK, such as information about signers, and verification errors and warnings
     */
    public ApkSigningBlockUtils.Result verify()
            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
        if (mApk == null || mZipSections == null) {
            throw new IllegalStateException(
                    "A non-null apk and zip sections must be specified to verify an APK's v3 "
                            + "signatures");
        }
        SignatureInfo signatureInfo =
                ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
        mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;

        DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
        DataSource centralDir =
                mApk.slice(
                        signatureInfo.centralDirOffset,
                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
        ByteBuffer eocd = signatureInfo.eocd;

        parseSigners();

        if (mResult.containsErrors()) {
            return mResult;
        }
        ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
                mContentDigestsToVerify, mResult);

        // make sure that the v3 signers cover the entire targeted sdk version ranges and that the
        // longest SigningCertificateHistory, if present, corresponds to the newest platform
        // versions
        SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
        for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
            sortedSigners.put(signer.maxSdkVersion, signer);
        }

        // first make sure there is neither overlap nor holes
        int firstMin = 0;
        int lastMax = 0;
        int lastLineageSize = 0;

        // while we're iterating through the signers, build up the list of lineages
        List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());

        for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
            int currentMin = signer.minSdkVersion;
            int currentMax = signer.maxSdkVersion;
            if (firstMin == 0) {
                // first round sets up our basis
                firstMin = currentMin;
            } else {
                // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
                // is targeting a development release.
                if (currentMin != (lastMax + 1)
                        && !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
                    mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
                    break;
                }
            }
            lastMax = currentMax;

            // also, while we're here, make sure that the lineage sizes only increase
            if (signer.signingCertificateLineage != null) {
                int currLineageSize = signer.signingCertificateLineage.size();
                if (currLineageSize < lastLineageSize) {
                    mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
                    break;
                }
                lastLineageSize = currLineageSize;
                lineages.add(signer.signingCertificateLineage);
            }
        }

        // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
        // then the max level only needs to support up to that sdk version for rotation.
        if (firstMin > mMinSdkVersion
                || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
                    ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
            mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
        }

        try {
            mResult.signingCertificateLineage =
                    SigningCertificateLineage.consolidateLineages(lineages);
        } catch (IllegalArgumentException e) {
            mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
        }
        if (!mResult.containsErrors()) {
            mResult.verified = true;
        }
        return mResult;
    }

    /**
     * Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
     * {@code signerInfos} of the provided {@code result}.
     *
     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
     * However, this does not verify the integrity of the rest of the APK but rather simply reports
     * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
     *
     * <p>This method adds one or more errors to the {@code result} if a verification error is
     * expected to be encountered on an Android platform version in the
     * {@code [minSdkVersion, maxSdkVersion]} range.
     */
    public static void parseSigners(
            ByteBuffer apkSignatureSchemeV3Block,
            Set<ContentDigestAlgorithm> contentDigestsToVerify,
            ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
        try {
            new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
                    .setResult(result)
                    .setContentDigestsToVerify(contentDigestsToVerify)
                    .setFullVerification(false)
                    .build()
                    .parseSigners();
        } catch (IOException | SignatureNotFoundException e) {
            // This should never occur since the apkSignatureSchemeV3Block was already provided.
            throw new IllegalStateException("An exception was encountered when attempting to parse"
                    + " the signers from the provided APK Signature Scheme v3 block", e);
        }
    }

    /**
     * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
     * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
     * returned {@link ApkSigningBlockUtils.Result}.
     *
     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
     * However, this does not verify the integrity of the rest of the APK but rather simply reports
     * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
     *
     * <p>This method adds one or more errors to the returned {@code Result} if a verification error
     * is encountered when parsing the signers.
     */
    public ApkSigningBlockUtils.Result parseSigners()
            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
        ByteBuffer signers;
        try {
            if (mApkSignatureSchemeV3Block == null) {
                SignatureInfo signatureInfo =
                        ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
                mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
            }
            signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
        } catch (ApkFormatException e) {
            mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
            return mResult;
        }
        if (!signers.hasRemaining()) {
            mResult.addError(Issue.V3_SIG_NO_SIGNERS);
            return mResult;
        }

        CertificateFactory certFactory;
        try {
            certFactory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
        }
        int signerCount = 0;
        while (signers.hasRemaining()) {
            int signerIndex = signerCount;
            signerCount++;
            ApkSigningBlockUtils.Result.SignerInfo signerInfo =
                    new ApkSigningBlockUtils.Result.SignerInfo();
            signerInfo.index = signerIndex;
            mResult.signers.add(signerInfo);
            try {
                ByteBuffer signer = getLengthPrefixedSlice(signers);
                parseSigner(signer, certFactory, signerInfo);
            } catch (ApkFormatException | BufferUnderflowException e) {
                signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
                return mResult;
            }
        }
        return mResult;
    }

    /**
     * Parses the provided signer block and populates the {@code result}.
     *
     * <p>This verifies signatures over {@code signed-data} contained in this block, as well as
     * the data contained therein, but does not verify the integrity of the rest of the APK. To
     * facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
     * These digests can then be used to verify the integrity of the APK.
     *
     * <p>This method adds one or more errors to the {@code result} if a verification error is
     * expected to be encountered on an Android platform version in the
     * {@code [minSdkVersion, maxSdkVersion]} range.
     */
    private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
            ApkSigningBlockUtils.Result.SignerInfo result)
            throws ApkFormatException, NoSuchAlgorithmException {
        ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
        byte[] signedDataBytes = new byte[signedData.remaining()];
        signedData.get(signedDataBytes);
        signedData.flip();
        result.signedData = signedDataBytes;

        int parsedMinSdkVersion = signerBlock.getInt();
        int parsedMaxSdkVersion = signerBlock.getInt();
        result.minSdkVersion = parsedMinSdkVersion;
        result.maxSdkVersion = parsedMaxSdkVersion;
        if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
            result.addError(
                    Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
        }
        ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
        byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);

        // Parse the signatures block and identify supported signatures
        int signatureCount = 0;
        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
        while (signatures.hasRemaining()) {
            signatureCount++;
            try {
                ByteBuffer signature = getLengthPrefixedSlice(signatures);
                int sigAlgorithmId = signature.getInt();
                byte[] sigBytes = readLengthPrefixedByteArray(signature);
                result.signatures.add(
                        new ApkSigningBlockUtils.Result.SignerInfo.Signature(
                                sigAlgorithmId, sigBytes));
                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
                if (signatureAlgorithm == null) {
                    result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
                    continue;
                }
                // TODO consider dropping deprecated signatures for v3 or modifying
                // getSignaturesToVerify (called below)
                supportedSignatures.add(
                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
            } catch (ApkFormatException | BufferUnderflowException e) {
                result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
                return;
            }
        }
        if (result.signatures.isEmpty()) {
            result.addError(Issue.V3_SIG_NO_SIGNATURES);
            return;
        }

        // Verify signatures over signed-data block using the public key
        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
        try {
            signaturesToVerify =
                    ApkSigningBlockUtils.getSignaturesToVerify(
                            supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
            result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
            return;
        }
        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
            String jcaSignatureAlgorithm =
                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
            String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
            PublicKey publicKey;
            try {
                publicKey =
                        KeyFactory.getInstance(keyAlgorithm).generatePublic(
                                new X509EncodedKeySpec(publicKeyBytes));
            } catch (Exception e) {
                result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
                return;
            }
            try {
                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
                sig.initVerify(publicKey);
                if (jcaSignatureAlgorithmParams != null) {
                    sig.setParameter(jcaSignatureAlgorithmParams);
                }
                signedData.position(0);
                sig.update(signedData);
                byte[] sigBytes = signature.signature;
                if (!sig.verify(sigBytes)) {
                    result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
                    return;
                }
                result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
                mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
            } catch (InvalidKeyException | InvalidAlgorithmParameterException
                    | SignatureException e) {
                result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
                return;
            }
        }

        // At least one signature over signedData has verified. We can now parse signed-data.
        signedData.position(0);
        ByteBuffer digests = getLengthPrefixedSlice(signedData);
        ByteBuffer certificates = getLengthPrefixedSlice(signedData);

        int signedMinSdkVersion = signedData.getInt();
        if (signedMinSdkVersion != parsedMinSdkVersion) {
            result.addError(
                    Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
                    parsedMinSdkVersion,
                    signedMinSdkVersion);
        }
        int signedMaxSdkVersion = signedData.getInt();
        if (signedMaxSdkVersion != parsedMaxSdkVersion) {
            result.addError(
                    Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
                    parsedMaxSdkVersion,
                    signedMaxSdkVersion);
        }
        ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);

        // Parse the certificates block
        int certificateIndex = -1;
        while (certificates.hasRemaining()) {
            certificateIndex++;
            byte[] encodedCert = readLengthPrefixedByteArray(certificates);
            X509Certificate certificate;
            try {
                certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
            } catch (CertificateException e) {
                result.addError(
                        Issue.V3_SIG_MALFORMED_CERTIFICATE,
                        certificateIndex,
                        certificateIndex + 1,
                        e);
                return;
            }
            // Wrap the cert so that the result's getEncoded returns exactly the original encoded
            // form. Without this, getEncoded may return a different form from what was stored in
            // the signature. This is because some X509Certificate(Factory) implementations
            // re-encode certificates.
            certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
            result.certs.add(certificate);
        }

        if (result.certs.isEmpty()) {
            result.addError(Issue.V3_SIG_NO_CERTIFICATES);
            return;
        }
        X509Certificate mainCertificate = result.certs.get(0);
        byte[] certificatePublicKeyBytes;
        try {
            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
                    mainCertificate.getPublicKey());
        } catch (InvalidKeyException e) {
            System.out.println("Caught an exception encoding the public key: " + e);
            e.printStackTrace();
            certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
        }
        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
            result.addError(
                    Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
                    ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
                    ApkSigningBlockUtils.toHex(publicKeyBytes));
            return;
        }

        // Parse the digests block
        int digestCount = 0;
        while (digests.hasRemaining()) {
            digestCount++;
            try {
                ByteBuffer digest = getLengthPrefixedSlice(digests);
                int sigAlgorithmId = digest.getInt();
                byte[] digestBytes = readLengthPrefixedByteArray(digest);
                result.contentDigests.add(
                        new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
                                sigAlgorithmId, digestBytes));
            } catch (ApkFormatException | BufferUnderflowException e) {
                result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
                return;
            }
        }

        List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
        for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
            sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
        }
        List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
            sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
        }

        if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
            result.addError(
                    Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
                    sigAlgsFromSignaturesRecord,
                    sigAlgsFromDigestsRecord);
            return;
        }

        // Parse the additional attributes block.
        int additionalAttributeCount = 0;
        boolean rotationAttrFound = false;
        while (additionalAttributes.hasRemaining()) {
            additionalAttributeCount++;
            try {
                ByteBuffer attribute =
                        getLengthPrefixedSlice(additionalAttributes);
                int id = attribute.getInt();
                byte[] value = ByteBufferUtils.toByteArray(attribute);
                result.additionalAttributes.add(
                        new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
                if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
                    try {
                        // SigningCertificateLineage is verified when built
                        result.signingCertificateLineage =
                                SigningCertificateLineage.readFromV3AttributeValue(value);
                        // make sure that the last cert in the chain matches this signer cert
                        SigningCertificateLineage subLineage =
                                result.signingCertificateLineage.getSubLineage(result.certs.get(0));
                        if (result.signingCertificateLineage.size() != subLineage.size()) {
                            result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
                        }
                    } catch (SecurityException e) {
                        result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
                    } catch (IllegalArgumentException e) {
                        result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
                    } catch (Exception e) {
                        result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
                    }
                } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
                    rotationAttrFound = true;
                    // API targeting for rotation was added with V3.1; if the maxSdkVersion
                    // does not support v3.1 then ignore this attribute.
                    if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
                            && mFullVerification) {
                        int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
                                .order(ByteOrder.LITTLE_ENDIAN).getInt();
                        if (mOptionalRotationMinSdkVersion.isPresent()) {
                            int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
                            if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
                                result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
                                    attrRotationMinSdkVersion, rotationMinSdkVersion);
                            }
                        } else {
                            result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
                        }
                    }
                } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
                    // This attribute should only be used by a v3.1 signer to indicate rotation
                    // is targeting the development release that is using the SDK version of the
                    // previously released platform version.
                    if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
                        result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
                    }
                } else {
                    result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
                }
            } catch (ApkFormatException | BufferUnderflowException e) {
                result.addError(
                        Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
                return;
            }
        }
        if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
            result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
                    mOptionalRotationMinSdkVersion.getAsInt());
        }
    }

    /**
     * Returns whether the specified {@code signerInfo} is targeting a development release.
     */
    public static boolean signerTargetsDevRelease(
            ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
        boolean result = signerInfo.additionalAttributes.stream()
                .mapToInt(attribute -> attribute.getId())
                .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
        return result;
    }

    /** Builder of {@link V3SchemeVerifier} instances. */
    public static class Builder {
        private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
        private DataSource mApk;
        private ApkUtils.ZipSections mZipSections;
        private ByteBuffer mApkSignatureSchemeV3Block;
        private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
        private ApkSigningBlockUtils.Result mResult;
        private int mMinSdkVersion;
        private int mMaxSdkVersion;
        private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
        private boolean mFullVerification = true;
        private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();

        /**
         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
         * verify the V3 signing block of the provided {@code apk} with the specified {@code
         * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
         */
        public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
                int maxSdkVersion) {
            mApk = apk;
            mZipSections = zipSections;
            mMinSdkVersion = minSdkVersion;
            mMaxSdkVersion = maxSdkVersion;
        }

        /**
         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
         * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
         * apkSignatureSchemeV3Block}.
         *
         * <note>Full verification of the v3 signature is not possible when instantiating a new
         * {@code V3SchemeVerifier} with this method.</note>
         */
        public Builder(ByteBuffer apkSignatureSchemeV3Block) {
            mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
        }

        /**
         * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
         */
        public Builder setRunnablesExecutor(RunnablesExecutor executor) {
            mExecutor = executor;
            return this;
        }

        /**
         * Sets the V3 {code blockId} to be verified in the provided APK.
         *
         * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
         */
        public Builder setBlockId(int blockId) {
            mBlockId = blockId;
            return this;
        }

        /**
         * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
         * attribute.
         *
         * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
         * block of an APK; in the case of multiple signers targeting different SDK versions in the
         * v3.1 signing block, the minimum SDK version from all the signers should be used.
         */
        public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
            mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
            return this;
        }

        /**
         * Sets the {@code result} instance to be used when returning verification results.
         *
         * <p>This method can be used when the caller already has a {@link
         * ApkSigningBlockUtils.Result} and wants to store the verification results in this
         * instance.
         */
        public Builder setResult(ApkSigningBlockUtils.Result result) {
            mResult = result;
            return this;
        }

        /**
         * Sets the instance to be used to store the {@code contentDigestsToVerify}.
         *
         * <p>This method can be used when the caller needs access to the {@code
         * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
         */
        public Builder setContentDigestsToVerify(
                Set<ContentDigestAlgorithm> contentDigestsToVerify) {
            mContentDigestsToVerify = contentDigestsToVerify;
            return this;
        }

        /**
         * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
         * from this instance.
         *
         * <note>{@link #verify()} will always verify the content digests for the APK, but this
         * allows verification of the rotation minimum SDK version stripping attribute to be skipped
         * for scenarios where this value may not have been parsed from a V3.1 signing block (such
         * as when only {@link #parseSigners()} will be invoked.</note>
         */
        public Builder setFullVerification(boolean fullVerification) {
            mFullVerification = fullVerification;
            return this;
        }

        /**
         * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
         * {@code Builder}.
         */
        public V3SchemeVerifier build() {
            int sigSchemeVersion;
            switch (mBlockId) {
                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
                    mMinSdkVersion = Math.max(mMinSdkVersion,
                            V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
                    break;
                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
                    // V3.1 supports targeting an SDK version later than that of the initial release
                    // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
                    // rest of the range.
                    mMinSdkVersion = mMaxSdkVersion;
                    break;
                default:
                    throw new IllegalArgumentException(
                            String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
                                    mBlockId));
            }
            if (mResult == null) {
                mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
            }
            if (mContentDigestsToVerify == null) {
                mContentDigestsToVerify = new HashSet<>(1);
            }

            V3SchemeVerifier verifier = new V3SchemeVerifier(
                    mExecutor,
                    mApk,
                    mZipSections,
                    mContentDigestsToVerify,
                    mResult,
                    mMinSdkVersion,
                    mMaxSdkVersion,
                    mBlockId,
                    mOptionalRotationMinSdkVersion,
                    mFullVerification);
            if (mApkSignatureSchemeV3Block != null) {
                verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
            }
            return verifier;
        }
    }
}