Merge "Don't depend on Bouncy Castle."
diff --git a/core/binary.mk b/core/binary.mk
index c3bf451..0c57030 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -720,6 +720,8 @@
     else
         my_shared_libraries += libprotobuf-cpp-full
     endif
+else ifeq ($(LOCAL_PROTOC_OPTIMIZE_TYPE),lite-static)
+    my_static_libraries += libprotobuf-cpp-lite
 else
     ifdef LOCAL_SDK_VERSION
         my_static_libraries += libprotobuf-cpp-lite-ndk
diff --git a/core/config.mk b/core/config.mk
index ba9f0cd..b179881 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -459,7 +459,6 @@
 prebuilt_sdk_tools := prebuilts/sdk/tools
 prebuilt_sdk_tools_bin := $(prebuilt_sdk_tools)/$(HOST_OS)/bin
 
-ACP := $(HOST_OUT_EXECUTABLES)/acp
 AIDL := $(HOST_OUT_EXECUTABLES)/aidl
 AAPT := $(HOST_OUT_EXECUTABLES)/aapt
 ZIPALIGN := $(HOST_OUT_EXECUTABLES)/zipalign
@@ -474,14 +473,16 @@
 # Always use prebuilts for ckati and makeparallel
 prebuilt_build_tools := prebuilts/build-tools
 prebuilt_build_tools_bin := $(prebuilt_build_tools)/$(HOST_PREBUILT_TAG)/bin
+ACP := $(prebuilt_build_tools_bin)/acp
 CKATI := $(prebuilt_build_tools_bin)/ckati
+IJAR := $(prebuilt_build_tools_bin)/ijar
 MAKEPARALLEL := $(prebuilt_build_tools_bin)/makeparallel
+ZIPTIME := $(prebuilt_build_tools_bin)/ziptime
 
 USE_PREBUILT_SDK_TOOLS_IN_PLACE := true
 
 # Override the definitions above for unbundled and PDK builds
 ifneq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)))
-ACP := $(prebuilt_sdk_tools_bin)/acp
 AIDL := $(prebuilt_sdk_tools_bin)/aidl
 AAPT := $(prebuilt_sdk_tools_bin)/aapt
 ZIPALIGN := $(prebuilt_sdk_tools_bin)/zipalign
@@ -584,13 +585,6 @@
 VBOOT_SIGNER := prebuilts/misc/scripts/vboot_signer/vboot_signer.sh
 FEC := $(HOST_OUT_EXECUTABLES)/fec
 
-ifndef TARGET_BUILD_APPS
-ZIPTIME := $(HOST_OUT_EXECUTABLES)/ziptime$(HOST_EXECUTABLE_SUFFIX)
-endif
-
-# ijar converts a .jar file to a smaller .jar file which only has its
-# interfaces.
-IJAR := $(HOST_OUT_EXECUTABLES)/ijar$(BUILD_EXECUTABLE_SUFFIX)
 DEXDUMP := $(HOST_OUT_EXECUTABLES)/dexdump2$(BUILD_EXECUTABLE_SUFFIX)
 
 # relocation packer
diff --git a/core/definitions.mk b/core/definitions.mk
index 4fc21d5..1b647aa 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -2450,11 +2450,9 @@
 
 # Remove dynamic timestamps from packages
 #
-ifndef TARGET_BUILD_APPS
 define remove-timestamps-from-package
 $(hide) $(ZIPTIME) $@
 endef
-endif
 
 # Uncompress shared libraries embedded in an apk.
 #
diff --git a/core/dex_preopt_libart.mk b/core/dex_preopt_libart.mk
index b286cb7..64ea226 100644
--- a/core/dex_preopt_libart.mk
+++ b/core/dex_preopt_libart.mk
@@ -11,6 +11,10 @@
 DEX2OAT := $(HOST_OUT_EXECUTABLES)/dex2oatd$(HOST_EXECUTABLE_SUFFIX)
 endif
 
+# Pass special classpath to skip uses library check.
+# Should modify build system to pass used libraries properly later.
+DEX2OAT_CLASSPATH := "&"
+
 DEX2OAT_DEPENDENCY += $(DEX2OAT)
 
 # Use the first preloaded-classes file in PRODUCT_COPY_FILES.
@@ -89,6 +93,7 @@
 $(hide) mkdir -p $(dir $(2))
 $(hide) ANDROID_LOG_TAGS="*:e" $(DEX2OAT) \
 	--runtime-arg -Xms$(DEX2OAT_XMS) --runtime-arg -Xmx$(DEX2OAT_XMX) \
+	--runtime-arg -classpath --runtime-arg $(DEX2OAT_CLASSPATH) \
 	--boot-image=$(PRIVATE_DEX_PREOPT_IMAGE_LOCATION) \
 	--dex-file=$(1) \
 	--dex-location=$(PRIVATE_DEX_LOCATION) \
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
index 931c7b2..a40607a 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
@@ -17,16 +17,24 @@
 package com.android.apksigner.core;
 
 import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm;
 import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm;
 import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksigner.core.internal.util.AndroidSdkVersion;
 import com.android.apksigner.core.util.DataSource;
 import com.android.apksigner.core.zip.ZipFormatException;
 
 import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * APK signature verifier which mimics the behavior of the Android platform.
@@ -36,6 +44,10 @@
  */
 public class ApkVerifier {
 
+    private static final int APK_SIGNATURE_SCHEME_V2_ID = 2;
+    private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
+            Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
+
     /**
      * Verifies the APK's signatures and returns the result of verification. The APK can be
      * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
@@ -53,23 +65,96 @@
 
         // Attempt to verify the APK using APK Signature Scheme v2
         Result result = new Result();
+        Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
         try {
             V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
+            foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
             result.mergeFrom(v2Result);
         } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
         if (result.containsErrors()) {
             return result;
         }
 
-        // TODO: Verify JAR signature if necessary
-        if (!result.isVerifiedUsingV2Scheme()) {
+        // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
+        // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
+        // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
+        // scheme) signatures were found.
+        if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
+            V1SchemeVerifier.Result v1Result =
+                    V1SchemeVerifier.verify(
+                            apk,
+                            zipSections,
+                            SUPPORTED_APK_SIG_SCHEME_NAMES,
+                            foundApkSigSchemeIds,
+                            minSdkVersion);
+            result.mergeFrom(v1Result);
+        }
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2
+        // signatures verified.
+        if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) {
+            ArrayList<Result.V1SchemeSignerInfo> v1Signers =
+                    new ArrayList<>(result.getV1SchemeSigners());
+            ArrayList<Result.V2SchemeSignerInfo> v2Signers =
+                    new ArrayList<>(result.getV2SchemeSigners());
+            ArrayList<ByteArray> v1SignerCerts = new ArrayList<>();
+            ArrayList<ByteArray> v2SignerCerts = new ArrayList<>();
+            for (Result.V1SchemeSignerInfo signer : v1Signers) {
+                try {
+                    v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+                } catch (CertificateEncodingException e) {
+                    throw new RuntimeException(
+                            "Failed to encode JAR signer " + signer.getName() + " certs", e);
+                }
+            }
+            for (Result.V2SchemeSignerInfo signer : v2Signers) {
+                try {
+                    v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+                } catch (CertificateEncodingException e) {
+                    throw new RuntimeException(
+                            "Failed to encode APK Signature Scheme v2 signer (index: "
+                                    + signer.getIndex() + ") certs",
+                            e);
+                }
+            }
+
+            for (int i = 0; i < v1SignerCerts.size(); i++) {
+                ByteArray v1Cert = v1SignerCerts.get(i);
+                if (!v2SignerCerts.contains(v1Cert)) {
+                    Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i);
+                    v1Signer.addError(Issue.V2_SIG_MISSING);
+                    break;
+                }
+            }
+            for (int i = 0; i < v2SignerCerts.size(); i++) {
+                ByteArray v2Cert = v2SignerCerts.get(i);
+                if (!v1SignerCerts.contains(v2Cert)) {
+                    Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i);
+                    v2Signer.addError(Issue.JAR_SIG_MISSING);
+                    break;
+                }
+            }
+        }
+        if (result.containsErrors()) {
             return result;
         }
 
         // Verified
         result.setVerified();
-        for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
-            result.addSignerCertificate(signerInfo.getCertificate());
+        if (result.isVerifiedUsingV2Scheme()) {
+            for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+                result.addSignerCertificate(signerInfo.getCertificate());
+            }
+        } else if (result.isVerifiedUsingV1Scheme()) {
+            for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) {
+                result.addSignerCertificate(signerInfo.getCertificate());
+            }
+        } else {
+            throw new RuntimeException(
+                    "APK considered verified, but has not verified using either v1 or v2 schemes");
         }
 
         return result;
@@ -83,9 +168,12 @@
         private final List<IssueWithParams> mErrors = new ArrayList<>();
         private final List<IssueWithParams> mWarnings = new ArrayList<>();
         private final List<X509Certificate> mSignerCerts = new ArrayList<>();
+        private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>();
+        private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
         private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
 
         private boolean mVerified;
+        private boolean mVerifiedUsingV1Scheme;
         private boolean mVerifiedUsingV2Scheme;
 
         /**
@@ -100,6 +188,13 @@
         }
 
         /**
+         * Returns {@code true} if the APK's JAR signatures verified.
+         */
+        public boolean isVerifiedUsingV1Scheme() {
+            return mVerifiedUsingV1Scheme;
+        }
+
+        /**
          * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
          */
         public boolean isVerifiedUsingV2Scheme() {
@@ -118,6 +213,27 @@
         }
 
         /**
+         * Returns information about JAR signers associated with the APK's signature. These are the
+         * signers used by Android.
+         *
+         * @see #getV1SchemeIgnoredSigners()
+         */
+        public List<V1SchemeSignerInfo> getV1SchemeSigners() {
+            return mV1SchemeSigners;
+        }
+
+        /**
+         * Returns information about JAR signers ignored by the APK's signature verification
+         * process. These signers are ignored by Android. However, each signer's errors or warnings
+         * will contain information about why they are ignored.
+         *
+         * @see #getV1SchemeSigners()
+         */
+        public List<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() {
+            return mV1SchemeIgnoredSigners;
+        }
+
+        /**
          * Returns information about APK Signature Scheme v2 signers associated with the APK's
          * signature.
          */
@@ -139,6 +255,18 @@
             return mWarnings;
         }
 
+        private void mergeFrom(V1SchemeVerifier.Result source) {
+            mVerifiedUsingV1Scheme = source.verified;
+            mErrors.addAll(source.getErrors());
+            mWarnings.addAll(source.getWarnings());
+            for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) {
+                mV1SchemeSigners.add(new V1SchemeSignerInfo(signer));
+            }
+            for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) {
+                mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer));
+            }
+        }
+
         private void mergeFrom(V2SchemeVerifier.Result source) {
             mVerifiedUsingV2Scheme = source.verified;
             mErrors.addAll(source.getErrors());
@@ -156,6 +284,13 @@
             if (!mErrors.isEmpty()) {
                 return true;
             }
+            if (!mV1SchemeSigners.isEmpty()) {
+                for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
             if (!mV2SchemeSigners.isEmpty()) {
                 for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
                     if (signer.containsErrors()) {
@@ -168,6 +303,98 @@
         }
 
         /**
+         * Information about a JAR signer associated with the APK's signature.
+         */
+        public static class V1SchemeSignerInfo {
+            private final String mName;
+            private final List<X509Certificate> mCertChain;
+            private final String mSignatureBlockFileName;
+            private final String mSignatureFileName;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+
+            private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) {
+                mName = result.name;
+                mCertChain = result.certChain;
+                mSignatureBlockFileName = result.signatureBlockFileName;
+                mSignatureFileName = result.signatureFileName;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+            }
+
+            /**
+             * Returns a user-friendly name of the signer.
+             */
+            public String getName() {
+                return mName;
+            }
+
+            /**
+             * Returns the name of the JAR entry containing this signer's JAR signature block file.
+             */
+            public String getSignatureBlockFileName() {
+                return mSignatureBlockFileName;
+            }
+
+            /**
+             * Returns the name of the JAR entry containing this signer's JAR signature file.
+             */
+            public String getSignatureFileName() {
+                return mSignatureFileName;
+            }
+
+            /**
+             * Returns this signer's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCertChain.isEmpty() ? null : mCertChain.get(0);
+            }
+
+            /**
+             * Returns the certificate chain for the signer's public key. The certificate containing
+             * the public key is first, followed by the certificate (if any) which issued the
+             * signing certificate, and so forth. An empty list may be returned if an error was
+             * encountered during verification (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificateChain() {
+                return mCertChain;
+            }
+
+            /**
+             * Returns {@code true} if an error was encountered while verifying this signer's JAR
+             * signature. Any error prevents the signer's signature from being considered verified.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            /**
+             * Returns errors encountered while verifying this signer's JAR signature. Any error
+             * prevents the signer's signature from being considered verified.
+             */
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            /**
+             * Returns warnings encountered while verifying this signer's JAR signature. Warnings
+             * do not prevent the signer's signature from being considered verified.
+             */
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+        }
+
+        /**
          * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
          */
         public static class V2SchemeSignerInfo {
@@ -212,6 +439,10 @@
                 return mCerts;
             }
 
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+
             public boolean containsErrors() {
                 return !mErrors.isEmpty();
             }
@@ -232,6 +463,324 @@
     public static enum Issue {
 
         /**
+         * APK is not JAR-signed.
+         */
+        JAR_SIG_NO_SIGNATURES("No JAR signatures"),
+
+        /**
+         * APK does not contain any entries covered by JAR signatures.
+         */
+        JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"),
+
+        /**
+         * APK contains multiple entries with the same name.
+         *
+         * <ul>
+         * <li>Parameter 1: name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
+
+        /**
+         * JAR manifest contains a section with a duplicate name.
+         *
+         * <ul>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
+
+        /**
+         * JAR manifest contains a section without a name.
+         *
+         * <ul>
+         * <li>Parameter 1: section index (1-based) ({@code Integer})</li>
+         * </ul>
+         */
+        JAR_SIG_UNNNAMED_MANIFEST_SECTION(
+                "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"),
+
+        /**
+         * JAR signature file contains a section without a name.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * <li>Parameter 2: section index (1-based) ({@code Integer})</li>
+         * </ul>
+         */
+        JAR_SIG_UNNNAMED_SIG_FILE_SECTION(
+                "Malformed %1$s: invidual section #%2$d does not have a name"),
+
+        /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */
+        JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"),
+
+        /**
+         * JAR manifest references an entry which is not there in the APK.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST(
+                "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"),
+
+        /**
+         * JAR manifest does not list a digest for the specified entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"),
+
+        /**
+         * JAR signature does not list a digest for the specified entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * <li>Parameter 2: signature file name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"),
+
+        /**
+         * The specified JAR entry is not covered by JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"),
+
+        /**
+         * JAR signature uses different set of signers to protect the two specified ZIP entries.
+         *
+         * <ul>
+         * <li>Parameter 1: first entry name ({@code String})</li>
+         * <li>Parameter 2: first entry signer names ({@code List<String>})</li>
+         * <li>Parameter 3: second entry name ({@code String})</li>
+         * <li>Parameter 4: second entry signer names ({@code List<String>})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH(
+                "Entries %1$s and %3$s are signed with different sets of signers"
+                        + " : <%2$s> vs <%4$s>"),
+
+        /**
+         * Digest of the specified ZIP entry's data does not match the digest expected by the JAR
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 3: name of the entry in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY(
+                "%2$s digest of %1$s does not match the digest specified in %3$s"
+                        + ". Expected: <%5$s>, actual: <%4$s>"),
+
+        /**
+         * Digest of the JAR manifest main section did not verify.
+         *
+         * <ul>
+         * <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 2: name of the entry in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 3: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 4: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY(
+                "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest"
+                        + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"),
+
+        /**
+         * Digest of the specified JAR manifest section does not match the digest expected by the
+         * JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+         * <li>Parameter 3: name of the signature file in which the expected digest is specified
+         *     ({@code String})</li>
+         * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+         * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY(
+                "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest"
+                        + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"),
+
+        /**
+         * JAR signature file does not contain the whole-file digest of the JAR manifest file. The
+         * digest speeds up verification of JAR signature.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE(
+                "%1$s does not specify digest of META-INF/MANIFEST.MF"
+                        + ". This slows down verification."),
+
+        /**
+         * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not
+         * contain protections against stripping of these newer scheme signatures.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_APK_SIG_STRIP_PROTECTION(
+                "APK is signed using APK Signature Scheme v2 but these signatures may be stripped"
+                        + " without being detected because %1$s does not contain anti-stripping"
+                        + " protections."),
+
+        /**
+         * JAR signature of the signer is missing a file/entry.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the encountered file ({@code String})</li>
+         * <li>Parameter 2: name of the missing file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"),
+
+        /**
+         * An exception was encountered while verifying JAR signature contained in a signature block
+         * against the signature file.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: name of the signature file ({@code String})</li>
+         * <li>Parameter 3: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
+
+        /**
+         * An exception was encountered while parsing JAR signature contained in a signature block.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"),
+
+        /**
+         * An exception was encountered while parsing a certificate contained in the JAR signature
+         * block.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"),
+
+        /**
+         * JAR signature contained in a signature block file did not verify against the signature
+         * file.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * <li>Parameter 2: name of the signature file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
+
+        /**
+         * JAR signature contains no verified signers.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature block file ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
+
+        /**
+         * JAR signature file contains a section with a duplicate name.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * <li>Parameter 1: section name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"),
+
+        /**
+         * JAR signature file's main section doesn't contain the mandatory Signature-Version
+         * attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: signature file name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE(
+                "Malformed %1$s: missing Signature-Version attribute"),
+
+        /**
+         * JAR signature file references an unknown APK signature scheme ID.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
+         * </ul>
+         */
+        JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+                "JAR signature %1$s references unknown APK signature scheme ID: %2$d"),
+
+        /**
+         * JAR signature file indicates that the APK is supposed to be signed with a supported APK
+         * signature scheme (in addition to the JAR signature) but no such signature was found in
+         * the APK.
+         *
+         * <ul>
+         * <li>Parameter 1: name of the signature file ({@code String})</li>
+         * <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li>
+         * <li>Parameter 3: APK signature scheme English name ({@code} String)</li>
+         * </ul>
+         */
+        JAR_SIG_MISSING_APK_SIG_REFERENCED(
+                "JAR signature %1$s indicates the APK is signed using %3$s but no such signature"
+                        + " was found. Signature stripped?"),
+
+        /**
+         * JAR entry is not covered by signature and thus unauthorized modifications to its contents
+         * will not be detected.
+         *
+         * <ul>
+         * <li>Parameter 1: entry name ({@code String})</li>
+         * </ul>
+         */
+        JAR_SIG_UNPROTECTED_ZIP_ENTRY(
+                "%1$s not protected by signature. Unauthorized modifications to this JAR entry"
+                        + " will not be detected. Delete or move the entry outside of META-INF/."),
+
+        /**
+         * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK
+         * Signature Scheme v2 signature from this signer, but does not contain a JAR signature
+         * from this signer.
+         */
+        JAR_SIG_MISSING(
+                "No APK Signature Scheme v2 signature from this signer despite APK being v2"
+                        + " signed"),
+
+        /**
+         * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
+         * signature from this signer, but does not contain an APK Signature Scheme v2 signature
+         * from this signer.
+         */
+        V2_SIG_MISSING(
+                "No APK Signature Scheme v2 signature from this signer despite APK being v2"
+                        + " signed"),
+
+        /**
          * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
          */
         V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
@@ -455,4 +1004,42 @@
             return String.format(mIssue.getFormat(), mParams);
         }
     }
+
+    /**
+     * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate
+     * on the contents of the arrays rather than on references.
+     */
+    private static class ByteArray {
+        private final byte[] mArray;
+
+        private ByteArray(byte[] arr) {
+            mArray = arr;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + Arrays.hashCode(mArray);
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            ByteArray other = (ByteArray) obj;
+            if (!Arrays.equals(mArray, other.mArray)) {
+                return false;
+            }
+            return true;
+        }
+    }
 }
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
index f89ed99..91d1990 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
@@ -67,8 +67,9 @@
     private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
     private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
 
+    static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
     private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
-            new Attributes.Name("X-Android-APK-Signed");
+            new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
 
     /**
      * Signer configuration.
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
new file mode 100644
index 0000000..762f5aa
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
@@ -0,0 +1,1172 @@
+/*
+ * Copyright (C) 2016 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.apksigner.core.internal.apk.v1;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.jar.Attributes;
+
+import com.android.apksigner.core.ApkVerifier.Issue;
+import com.android.apksigner.core.ApkVerifier.IssueWithParams;
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.internal.jar.ManifestParser;
+import com.android.apksigner.core.internal.util.AndroidSdkVersion;
+import com.android.apksigner.core.internal.util.MessageDigestSink;
+import com.android.apksigner.core.internal.zip.CentralDirectoryRecord;
+import com.android.apksigner.core.internal.zip.LocalFileHeader;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.SignerInfo;
+
+/**
+ * APK verifier which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeVerifier {
+
+    private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME;
+
+    private V1SchemeVerifier() {}
+
+    /**
+     * Verifies the provided APK's JAR signatures and returns the result of verification. APK is
+     * considered verified only if {@link Result#verified} is {@code true}. If verification fails,
+     * the result will contain errors -- see {@link Result#getErrors()}.
+     *
+     * @throws ZipFormatException if the APK is malformed
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections apkSections,
+            Map<Integer, String> supportedApkSigSchemeNames,
+            Set<Integer> foundApkSigSchemeIds,
+            int minSdkVersion) throws IOException, ZipFormatException {
+        Result result = new Result();
+
+        // Parse the ZIP Central Directory and check that there are no entries with duplicate names.
+        List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections);
+        Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result);
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Verify JAR signature(s).
+        Signers.verify(
+                apk,
+                apkSections.getZipCentralDirectoryOffset(),
+                cdRecords,
+                cdEntryNames,
+                supportedApkSigSchemeNames,
+                foundApkSigSchemeIds,
+                minSdkVersion,
+                result);
+
+        return result;
+    }
+
+    /**
+     * Returns the set of entry names and reports any duplicate entry names in the {@code result}
+     * as errors.
+     */
+    private static Set<String> checkForDuplicateEntries(
+            List<CentralDirectoryRecord> cdRecords, Result result) {
+        Set<String> cdEntryNames = new HashSet<>(cdRecords.size());
+        Set<String> duplicateCdEntryNames = null;
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            String entryName = cdRecord.getName();
+            if (!cdEntryNames.add(entryName)) {
+                // This is an error. Report this once per duplicate name.
+                if (duplicateCdEntryNames == null) {
+                    duplicateCdEntryNames = new HashSet<>();
+                }
+                if (duplicateCdEntryNames.add(entryName)) {
+                    result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName);
+                }
+            }
+        }
+        return cdEntryNames;
+    }
+
+    /**
+     * All JAR signers of an APK.
+     */
+    private static class Signers {
+
+        /**
+         * Verifies JAR signatures of the provided APK and populates the provided result container
+         * with errors, warnings, and information about signers. The APK is considered verified if
+         * the {@link Result#verified} is {@code true}.
+         */
+        private static void verify(
+                DataSource apk,
+                long cdStartOffset,
+                List<CentralDirectoryRecord> cdRecords,
+                Set<String> cdEntryNames,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds,
+                int minSdkVersion,
+                Result result) throws ZipFormatException, IOException {
+
+            // Find JAR manifest and signature block files.
+            CentralDirectoryRecord manifestEntry = null;
+            Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
+            List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                String entryName = cdRecord.getName();
+                if (!entryName.startsWith("META-INF/")) {
+                    continue;
+                }
+                if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) {
+                    manifestEntry = cdRecord;
+                    continue;
+                }
+                if (entryName.endsWith(".SF")) {
+                    sigFileEntries.put(entryName, cdRecord);
+                    continue;
+                }
+                if ((entryName.endsWith(".RSA"))
+                        || (entryName.endsWith(".DSA"))
+                        || (entryName.endsWith(".EC"))) {
+                    sigBlockEntries.add(cdRecord);
+                    continue;
+                }
+            }
+            if (manifestEntry == null) {
+                result.addError(Issue.JAR_SIG_NO_MANIFEST);
+                return;
+            }
+
+            // Parse the JAR manifest and check that all JAR entries it references exist in the APK.
+            byte[] manifestBytes =
+                    LocalFileHeader.getUncompressedData(
+                            apk, 0,
+                            manifestEntry,
+                            cdStartOffset);
+            Map<String, ManifestParser.Section> entryNameToManifestSection = null;
+            ManifestParser manifest = new ManifestParser(manifestBytes);
+            ManifestParser.Section manifestMainSection = manifest.readSection();
+            List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections();
+            entryNameToManifestSection = new HashMap<>(manifestIndividualSections.size());
+            int manifestSectionNumber = 0;
+            for (ManifestParser.Section manifestSection : manifestIndividualSections) {
+                manifestSectionNumber++;
+                String entryName = manifestSection.getName();
+                if (entryName == null) {
+                    result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber);
+                    continue;
+                }
+                if (entryNameToManifestSection.put(entryName, manifestSection) != null) {
+                    result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName);
+                    continue;
+                }
+                if (!cdEntryNames.contains(entryName)) {
+                    result.addError(
+                            Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName);
+                    continue;
+                }
+            }
+            if (result.containsErrors()) {
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All JAR entries listed in JAR manifest are present in the APK.
+
+            // Identify signers
+            List<Signer> signers = new ArrayList<>(sigBlockEntries.size());
+            for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) {
+                String sigBlockEntryName = sigBlockEntry.getName();
+                int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.');
+                if (extensionDelimiterIndex == -1) {
+                    throw new RuntimeException(
+                            "Signature block file name does not contain extension: "
+                                    + sigBlockEntryName);
+                }
+                String sigFileEntryName =
+                        sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF";
+                CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName);
+                if (sigFileEntry == null) {
+                    result.addWarning(
+                            Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName);
+                    continue;
+                }
+                String signerName = sigBlockEntryName.substring("META-INF/".length());
+                Result.SignerInfo signerInfo =
+                        new Result.SignerInfo(
+                                signerName, sigBlockEntryName, sigFileEntry.getName());
+                Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo);
+                signers.add(signer);
+            }
+            if (signers.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_SIGNATURES);
+                return;
+            }
+
+            // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding
+            // signature file .SF. Any error encountered for any signer terminates verification, to
+            // mimic Android's behavior.
+            for (Signer signer : signers) {
+                signer.verifySigBlockAgainstSigFile(apk, cdStartOffset, minSdkVersion);
+                if (signer.getResult().containsErrors()) {
+                    result.signers.add(signer.getResult());
+                }
+            }
+            if (result.containsErrors()) {
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All JAR entries listed in JAR manifest are present in the APK.
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+
+            // Verify each signer's signature file (.SF) against the JAR manifest.
+            List<Signer> remainingSigners = new ArrayList<>(signers.size());
+            for (Signer signer : signers) {
+                signer.verifySigFileAgainstManifest(
+                        manifestBytes,
+                        manifestMainSection,
+                        entryNameToManifestSection,
+                        supportedApkSigSchemeNames,
+                        foundApkSigSchemeIds,
+                        minSdkVersion);
+                if (signer.isIgnored()) {
+                    result.ignoredSigners.add(signer.getResult());
+                } else {
+                    if (signer.getResult().containsErrors()) {
+                        result.signers.add(signer.getResult());
+                    } else {
+                        remainingSigners.add(signer);
+                    }
+                }
+            }
+            if (result.containsErrors()) {
+                return;
+            }
+            signers = remainingSigners;
+            if (signers.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_SIGNATURES);
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+            // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
+            // * All JAR entries listed in JAR manifest are present in the APK.
+
+            // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's
+            // JAR entry is considered signed by signers associated with an .SF file iff the entry
+            // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest
+            // match theentry's uncompressed data. Android requires that all such JAR entries are
+            // signed by the same set of signers. This set may be smaller than the set of signers
+            // we've identified so far.
+            Set<Signer> apkSigners =
+                    verifyJarEntriesAgainstManifestAndSigners(
+                            apk,
+                            cdStartOffset,
+                            cdRecords,
+                            entryNameToManifestSection,
+                            signers,
+                            result);
+            if (result.containsErrors()) {
+                return;
+            }
+            // STATE OF AFFAIRS:
+            // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
+            // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
+            // * All JAR entries listed in JAR manifest are present in the APK.
+            // * All JAR entries present in the APK and supposed to be covered by JAR signature
+            //   (i.e., reside outside of META-INF/) are covered by signatures from the same set
+            //   of signers.
+
+            // Report any JAR entries which aren't covered by signature.
+            Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2);
+            signatureEntryNames.add(manifestEntry.getName());
+            for (Signer signer : apkSigners) {
+                signatureEntryNames.add(signer.getSignatureBlockEntryName());
+                signatureEntryNames.add(signer.getSignatureFileEntryName());
+            }
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                String entryName = cdRecord.getName();
+                if ((entryName.startsWith("META-INF/"))
+                        && (!entryName.endsWith("/"))
+                        && (!signatureEntryNames.contains(entryName))) {
+                    result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName);
+                }
+            }
+
+            // Reflect the sets of used signers and ignored signers in the result.
+            for (Signer signer : signers) {
+                if (apkSigners.contains(signer)) {
+                    result.signers.add(signer.getResult());
+                } else {
+                    result.ignoredSigners.add(signer.getResult());
+                }
+            }
+
+            result.verified = true;
+        }
+    }
+
+    private static class Signer {
+        private final String mName;
+        private final Result.SignerInfo mResult;
+        private final CentralDirectoryRecord mSignatureFileEntry;
+        private final CentralDirectoryRecord mSignatureBlockEntry;
+        private boolean mIgnored;
+
+        private byte[] mSigFileBytes;
+        private Set<String> mSigFileEntryNames;
+
+        private Signer(
+                String name,
+                CentralDirectoryRecord sigBlockEntry,
+                CentralDirectoryRecord sigFileEntry,
+                Result.SignerInfo result) {
+            mName = name;
+            mResult = result;
+            mSignatureBlockEntry = sigBlockEntry;
+            mSignatureFileEntry = sigFileEntry;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String getSignatureFileEntryName() {
+            return mSignatureFileEntry.getName();
+        }
+
+        public String getSignatureBlockEntryName() {
+            return mSignatureBlockEntry.getName();
+        }
+
+        void setIgnored() {
+            mIgnored = true;
+        }
+
+        public boolean isIgnored() {
+            return mIgnored;
+        }
+
+        public Set<String> getSigFileEntryNames() {
+            return mSigFileEntryNames;
+        }
+
+        public Result.SignerInfo getResult() {
+            return mResult;
+        }
+
+        @SuppressWarnings("restriction")
+        public void verifySigBlockAgainstSigFile(
+                DataSource apk, long cdStartOffset, int minSdkVersion)
+                        throws IOException, ZipFormatException {
+            byte[] sigBlockBytes =
+                    LocalFileHeader.getUncompressedData(
+                            apk, 0,
+                            mSignatureBlockEntry,
+                            cdStartOffset);
+            mSigFileBytes =
+                    LocalFileHeader.getUncompressedData(
+                            apk, 0,
+                            mSignatureFileEntry,
+                            cdStartOffset);
+            PKCS7 sigBlock;
+            try {
+                sigBlock = new PKCS7(sigBlockBytes);
+            } catch (IOException e) {
+                if (e.getCause() instanceof CertificateException) {
+                    mResult.addError(
+                            Issue.JAR_SIG_MALFORMED_CERTIFICATE, mSignatureBlockEntry.getName(), e);
+                } else {
+                    mResult.addError(Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                }
+                return;
+            }
+            SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos();
+            if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) {
+                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
+                return;
+            }
+
+            SignerInfo verifiedSignerInfo = null;
+            if ((unverifiedSignerInfos != null) && (unverifiedSignerInfos.length > 0)) {
+                for (int i = 0; i < unverifiedSignerInfos.length; i++) {
+                    SignerInfo unverifiedSignerInfo = unverifiedSignerInfos[i];
+                    // TODO: Reject sig/dig algorithms not supported on Android
+                    try {
+                        verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes);
+                    } catch (NoSuchAlgorithmException | SignatureException e) {
+                        mResult.addError(
+                                Issue.JAR_SIG_VERIFY_EXCEPTION,
+                                mSignatureBlockEntry.getName(),
+                                mSignatureFileEntry.getName(),
+                                e);
+                        return;
+                    }
+                    if (verifiedSignerInfo != null) {
+                        // Verified
+                        break;
+                    }
+
+                    // Did not verify
+                    if (minSdkVersion < AndroidSdkVersion.N) {
+                        // Prior to N, Android attempted to verify only the first SignerInfo.
+                        mResult.addError(
+                                Issue.JAR_SIG_DID_NOT_VERIFY,
+                                mSignatureBlockEntry.getName(),
+                                mSignatureFileEntry.getName());
+                        return;
+                    }
+                }
+            }
+            if (verifiedSignerInfo == null) {
+                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
+                return;
+            }
+
+            List<X509Certificate> certChain;
+            try {
+                certChain = verifiedSignerInfo.getCertificateChain(sigBlock);
+            } catch (IOException e) {
+                throw new RuntimeException(
+                        "Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e);
+            }
+            if ((certChain == null) || (certChain.isEmpty())) {
+                throw new RuntimeException("Verified SignerInfo does not have a certificate chain");
+            }
+            mResult.certChain.clear();
+            mResult.certChain.addAll(certChain);
+        }
+
+        public void verifySigFileAgainstManifest(
+                byte[] manifestBytes,
+                ManifestParser.Section manifestMainSection,
+                Map<String, ManifestParser.Section> entryNameToManifestSection,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds,
+                int minSdkVersion) {
+            // Inspect the main section of the .SF file.
+            ManifestParser sf = new ManifestParser(mSigFileBytes);
+            ManifestParser.Section sfMainSection = sf.readSection();
+            if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) {
+                mResult.addError(
+                        Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE,
+                        mSignatureFileEntry.getName());
+                setIgnored();
+                return;
+            }
+            checkForStrippedApkSignatures(
+                    sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
+            if (mResult.containsErrors()) {
+                return;
+            }
+
+            boolean createdBySigntool = false;
+            String createdBy = sfMainSection.getAttributeValue("Created-By");
+            if (createdBy != null) {
+                createdBySigntool = createdBy.indexOf("signtool") != -1;
+            }
+            boolean manifestDigestVerified =
+                    verifyManifestDigest(
+                            sfMainSection, createdBySigntool, manifestBytes, minSdkVersion);
+            if (!createdBySigntool) {
+                verifyManifestMainSectionDigest(
+                        sfMainSection, manifestMainSection, manifestBytes, minSdkVersion);
+            }
+            if (mResult.containsErrors()) {
+                return;
+            }
+
+            // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest
+            // verifies, per-entry sections should be ignored. However, most Android platform
+            // implementations require that such sections exist.
+            List<ManifestParser.Section> sfSections = sf.readAllSections();
+            Set<String> sfEntryNames = new HashSet<>(sfSections.size());
+            int sfSectionNumber = 0;
+            for (ManifestParser.Section sfSection : sfSections) {
+                sfSectionNumber++;
+                String entryName = sfSection.getName();
+                if (entryName == null) {
+                    mResult.addError(
+                            Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION,
+                            mSignatureFileEntry.getName(),
+                            sfSectionNumber);
+                    setIgnored();
+                    return;
+                }
+                if (!sfEntryNames.add(entryName)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION,
+                            mSignatureFileEntry.getName(),
+                            entryName);
+                    setIgnored();
+                    return;
+                }
+                if (manifestDigestVerified) {
+                    // No need to verify this entry's corresponding JAR manifest entry because the
+                    // JAR manifest verifies in full.
+                    continue;
+                }
+                // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify
+                // the digest of the JAR manifest section corresponding to this .SF section.
+                ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
+                if (manifestSection == null) {
+                    mResult.addError(
+                            Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
+                            entryName,
+                            mSignatureFileEntry.getName());
+                    setIgnored();
+                    continue;
+                }
+                verifyManifestIndividualSectionDigest(
+                        sfSection,
+                        createdBySigntool,
+                        manifestSection,
+                        manifestBytes,
+                        minSdkVersion);
+            }
+            mSigFileEntryNames = sfEntryNames;
+        }
+
+
+        /**
+         * Returns {@code true} if the whole-file digest of the manifest against the main section of
+         * the .SF file.
+         */
+        private boolean verifyManifestDigest(
+                ManifestParser.Section sfMainSection,
+                boolean createdBySigntool,
+                byte[] manifestBytes,
+                int minSdkVersion) {
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(
+                            sfMainSection,
+                            ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
+                            minSdkVersion);
+            boolean digestFound = !expectedDigests.isEmpty();
+            if (!digestFound) {
+                mResult.addWarning(
+                        Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE,
+                        mSignatureFileEntry.getName());
+                return false;
+            }
+
+            boolean verified = true;
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual = digest(jcaDigestAlgorithm, manifestBytes);
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addWarning(
+                            Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
+                            V1SchemeSigner.MANIFEST_ENTRY_NAME,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                    verified = false;
+                }
+            }
+            return verified;
+        }
+
+        /**
+         * Verifies the digest of the manifest's main section against the main section of the .SF
+         * file.
+         */
+        private void verifyManifestMainSectionDigest(
+                ManifestParser.Section sfMainSection,
+                ManifestParser.Section manifestMainSection,
+                byte[] manifestBytes,
+                int minSdkVersion) {
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(
+                            sfMainSection, "-Digest-Manifest-Main-Attributes", minSdkVersion);
+            if (expectedDigests.isEmpty()) {
+                return;
+            }
+
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual =
+                        digest(
+                                jcaDigestAlgorithm,
+                                manifestBytes,
+                                manifestMainSection.getStartOffset(),
+                                manifestMainSection.getSizeBytes());
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                }
+            }
+        }
+
+        /**
+         * Verifies the digest of the manifest's individual section against the corresponding
+         * individual section of the .SF file.
+         */
+        private void verifyManifestIndividualSectionDigest(
+                ManifestParser.Section sfIndividualSection,
+                boolean createdBySigntool,
+                ManifestParser.Section manifestIndividualSection,
+                byte[] manifestBytes,
+                int minSdkVersion) {
+            String entryName = sfIndividualSection.getName();
+            Collection<NamedDigest> expectedDigests =
+                    getDigestsToVerify(sfIndividualSection, "-Digest", minSdkVersion);
+            if (expectedDigests.isEmpty()) {
+                mResult.addError(
+                        Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
+                        entryName,
+                        mSignatureFileEntry.getName());
+                return;
+            }
+
+            int sectionStartIndex = manifestIndividualSection.getStartOffset();
+            int sectionSizeBytes = manifestIndividualSection.getSizeBytes();
+            if (createdBySigntool) {
+                int sectionEndIndex = sectionStartIndex + sectionSizeBytes;
+                if ((manifestBytes[sectionEndIndex - 1] == '\n')
+                        && (manifestBytes[sectionEndIndex - 2] == '\n')) {
+                    sectionSizeBytes--;
+                }
+            }
+            for (NamedDigest expectedDigest : expectedDigests) {
+                String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
+                byte[] actual =
+                        digest(
+                                jcaDigestAlgorithm,
+                                manifestBytes,
+                                sectionStartIndex,
+                                sectionSizeBytes);
+                byte[] expected = expectedDigest.digest;
+                if (!Arrays.equals(expected, actual)) {
+                    mResult.addError(
+                            Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY,
+                            entryName,
+                            jcaDigestAlgorithm,
+                            mSignatureFileEntry.getName(),
+                            Base64.getEncoder().encodeToString(actual),
+                            Base64.getEncoder().encodeToString(expected));
+                }
+            }
+        }
+
+        private void checkForStrippedApkSignatures(
+                ManifestParser.Section sfMainSection,
+                Map<Integer, String> supportedApkSigSchemeNames,
+                Set<Integer> foundApkSigSchemeIds) {
+            String signedWithApkSchemes =
+                    sfMainSection.getAttributeValue(
+                            V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+            // This field contains a comma-separated list of APK signature scheme IDs which were
+            // used to sign this APK. Android rejects APKs where an ID is known to the platform but
+            // the APK didn't verify using that scheme.
+
+            if (signedWithApkSchemes == null) {
+                // APK signature (e.g., v2 scheme) stripping protections not enabled.
+                if (!foundApkSigSchemeIds.isEmpty()) {
+                    // APK is signed with an APK signature scheme such as v2 scheme.
+                    mResult.addWarning(
+                            Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION,
+                            mSignatureFileEntry.getName());
+                }
+                return;
+            }
+
+            if (supportedApkSigSchemeNames.isEmpty()) {
+                return;
+            }
+
+            Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
+            Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
+            StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ",");
+            while (tokenizer.hasMoreTokens()) {
+                String idText = tokenizer.nextToken().trim();
+                if (idText.isEmpty()) {
+                    continue;
+                }
+                int id;
+                try {
+                    id = Integer.parseInt(idText);
+                } catch (Exception ignored) {
+                    continue;
+                }
+                // This APK was supposed to be signed with the APK signature scheme having
+                // this ID.
+                if (supportedApkSigSchemeIds.contains(id)) {
+                    supportedExpectedApkSigSchemeIds.add(id);
+                } else {
+                    mResult.addWarning(
+                            Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID,
+                            mSignatureFileEntry.getName(),
+                            id);
+                }
+            }
+
+            for (int id : supportedExpectedApkSigSchemeIds) {
+                if (!foundApkSigSchemeIds.contains(id)) {
+                    String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
+                    mResult.addError(
+                            Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED,
+                            mSignatureFileEntry.getName(),
+                            id,
+                            apkSigSchemeName);
+                }
+            }
+        }
+    }
+
+    private static Collection<NamedDigest> getDigestsToVerify(
+            ManifestParser.Section section,
+            String digestAttrSuffix,
+            int minSdkVersion) {
+        Decoder base64Decoder = Base64.getDecoder();
+        List<NamedDigest> result = new ArrayList<>(1);
+        if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) {
+            // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is
+            // to rely on the ancient Digest-Algorithms attribute which contains
+            // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The
+            // first digest attribute (with supported digest algorithm) found using the list is
+            // used.
+            String algs = section.getAttributeValue("Digest-Algorithms");
+            if (algs == null) {
+                algs = "SHA SHA1";
+            }
+            StringTokenizer tokens = new StringTokenizer(algs);
+            while (tokens.hasMoreTokens()) {
+                String alg = tokens.nextToken();
+                String attrName = alg + digestAttrSuffix;
+                String digestBase64 = section.getAttributeValue(attrName);
+                if (digestBase64 == null) {
+                    // Attribute not found
+                    continue;
+                }
+                alg = getCanonicalJcaMessageDigestAlgorithm(alg);
+                if ((alg == null)
+                        || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg)
+                                > minSdkVersion)) {
+                    // Unsupported digest algorithm
+                    continue;
+                }
+                // Supported digest algorithm
+                result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64)));
+                break;
+            }
+            // No supported digests found -- this will fail to verify on pre-JB MR2 Androids.
+            if (result.isEmpty()) {
+                return result;
+            }
+        }
+
+        // JB MR2 and newer, Android platform picks the strongest algorithm out of:
+        // SHA-512, SHA-384, SHA-256, SHA-1.
+        for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
+            String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
+            String digestBase64 = section.getAttributeValue(attrName);
+            if (digestBase64 == null) {
+                // Attribute not found
+                continue;
+            }
+            byte[] digest = base64Decoder.decode(digestBase64);
+            byte[] digestInResult = getDigest(result, alg);
+            if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) {
+                result.add(new NamedDigest(alg, digest));
+            }
+            break;
+        }
+
+        return result;
+    }
+
+    private static String[] JB_MR2_AND_NEWER_DIGEST_ALGS = {
+            "SHA-512",
+            "SHA-384",
+            "SHA-256",
+            "SHA-1",
+    };
+
+    private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) {
+        return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US));
+    }
+
+    public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(
+            String jcaAlgorithmName) {
+        Integer result =
+                MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get(
+                        jcaAlgorithmName.toUpperCase(Locale.US));
+        return (result != null) ? result : Integer.MAX_VALUE;
+    }
+
+    private static String getJarDigestAttributeName(
+            String jcaDigestAlgorithm, String attrNameSuffix) {
+        if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) {
+            return "SHA1" + attrNameSuffix;
+        } else {
+            return jcaDigestAlgorithm + attrNameSuffix;
+        }
+    }
+
+    private static Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL;
+    static {
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8);
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384");
+        UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512");
+    }
+
+    private static Map<String, Integer> MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST;
+    static {
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-384", 9);
+        MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-512", 9);
+    }
+
+    private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) {
+        for (NamedDigest digest : digests) {
+            if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) {
+                return digest.digest;
+            }
+        }
+        return null;
+    }
+
+    private static List<CentralDirectoryRecord> parseZipCentralDirectory(
+            DataSource apk,
+            ApkUtils.ZipSections apkSections)
+                    throws IOException, ZipFormatException {
+        // Read the ZIP Central Directory
+        long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+        if (cdSizeBytes > Integer.MAX_VALUE) {
+            throw new ZipFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+        }
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+        cd.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Parse the ZIP Central Directory
+        int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+        List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+        for (int i = 0; i < expectedCdRecordCount; i++) {
+            CentralDirectoryRecord cdRecord;
+            int offsetInsideCd = cd.position();
+            try {
+                cdRecord = CentralDirectoryRecord.getRecord(cd);
+            } catch (ZipFormatException e) {
+                throw new ZipFormatException(
+                        "Failed to parse Central Directory record #" + (i + 1)
+                                + " at file offset " + (cdOffset + offsetInsideCd),
+                        e);
+            }
+            String entryName = cdRecord.getName();
+            if (entryName.endsWith("/")) {
+                // Ignore directory entries
+                continue;
+            }
+            cdRecords.add(cdRecord);
+        }
+        // There may be more data in Central Directory, but we don't warn or throw because Android
+        // ignores unused CD data.
+
+        return cdRecords;
+    }
+
+    /**
+     * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+     * manifest for the APK to verify on Android.
+     */
+    private static boolean isJarEntryDigestNeededInManifest(String entryName) {
+        // NOTE: This logic is different from what's required by the JAR signing scheme. This is
+        // because Android's APK verification logic differs from that spec. In particular, JAR
+        // signing spec includes into JAR manifest all files in subdirectories of META-INF and
+        // any files inside META-INF not related to signatures.
+        if (entryName.startsWith("META-INF/")) {
+            return false;
+        }
+        return !entryName.endsWith("/");
+    }
+
+    private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners(
+            DataSource apk,
+            long cdOffsetInApk,
+            Collection<CentralDirectoryRecord> cdRecords,
+            Map<String, ManifestParser.Section> entryNameToManifestSection,
+            List<Signer> signers,
+            Result result) throws ZipFormatException, IOException {
+        // Iterate over APK contents as sequentially as possible to improve performance.
+        List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset =
+                new ArrayList<>(cdRecords);
+        Collections.sort(
+                cdRecordsSortedByLocalFileHeaderOffset,
+                CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+        Set<String> manifestEntryNamesMissingFromApk =
+                new HashSet<>(entryNameToManifestSection.keySet());
+        List<Signer> firstSignedEntrySigners = null;
+        String firstSignedEntryName = null;
+        for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
+            String entryName = cdRecord.getName();
+            manifestEntryNamesMissingFromApk.remove(entryName);
+            if (!isJarEntryDigestNeededInManifest(entryName)) {
+                continue;
+            }
+
+            ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
+            if (manifestSection == null) {
+                result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
+                continue;
+            }
+
+            List<Signer> entrySigners = new ArrayList<>(signers.size());
+            for (Signer signer : signers) {
+                if (signer.getSigFileEntryNames().contains(entryName)) {
+                    entrySigners.add(signer);
+                }
+            }
+            if (entrySigners.isEmpty()) {
+                result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName);
+                continue;
+            }
+            if (firstSignedEntrySigners == null) {
+                firstSignedEntrySigners = entrySigners;
+                firstSignedEntryName = entryName;
+            } else if (!entrySigners.equals(firstSignedEntrySigners)) {
+                result.addError(
+                        Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH,
+                        firstSignedEntryName,
+                        getSignerNames(firstSignedEntrySigners),
+                        entryName,
+                        getSignerNames(entrySigners));
+                continue;
+            }
+
+            List<NamedDigest> expectedDigests = new ArrayList<>();
+            for (ManifestParser.Attribute attr : manifestSection.getAttributes()) {
+                String name = attr.getName();
+                String nameUpperCase = name.toUpperCase(Locale.US);
+                if (!nameUpperCase.endsWith("-DIGEST")) {
+                    continue;
+                }
+                String jcaDigestAlgorithm =
+                        nameUpperCase.substring(0, nameUpperCase.length() - "-DIGEST".length());
+                if ("SHA1".equals(jcaDigestAlgorithm)) {
+                    jcaDigestAlgorithm = "SHA-1";
+                }
+                byte[] digest = Base64.getDecoder().decode(attr.getValue());
+                expectedDigests.add(new NamedDigest(jcaDigestAlgorithm, digest));
+            }
+
+            if (expectedDigests.isEmpty()) {
+                result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
+                continue;
+            }
+
+            MessageDigest[] mds = new MessageDigest[expectedDigests.size()];
+            int mdIndex = 0;
+            for (NamedDigest expectedDigest : expectedDigests) {
+                mds[mdIndex] = getMessageDigest(expectedDigest.jcaDigestAlgorithm);
+                mdIndex++;
+            }
+
+            try {
+                LocalFileHeader.sendUncompressedData(
+                        apk, 0,
+                        cdRecord,
+                        cdOffsetInApk,
+                        new MessageDigestSink(mds));
+            } catch (ZipFormatException e) {
+                throw new ZipFormatException("Malformed entry: " + entryName, e);
+            } catch (IOException e) {
+                throw new IOException("Failed to read entry: " + entryName, e);
+            }
+
+            mdIndex = 0;
+            for (NamedDigest expectedDigest : expectedDigests) {
+                byte[] actualDigest = mds[mdIndex].digest();
+                if (!Arrays.equals(expectedDigest.digest, actualDigest)) {
+                    result.addError(
+                            Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
+                            entryName,
+                            expectedDigest.jcaDigestAlgorithm,
+                            V1SchemeSigner.MANIFEST_ENTRY_NAME,
+                            Base64.getEncoder().encodeToString(actualDigest),
+                            Base64.getEncoder().encodeToString(expectedDigest.digest));
+                }
+            }
+        }
+
+        if (firstSignedEntrySigners == null) {
+            result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES);
+            return Collections.emptySet();
+        } else {
+            return new HashSet<>(firstSignedEntrySigners);
+        }
+    }
+
+    private static List<String> getSignerNames(List<Signer> signers) {
+        if (signers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> result = new ArrayList<>(signers.size());
+        for (Signer signer : signers) {
+            result.add(signer.getName());
+        }
+        return result;
+    }
+
+    private static MessageDigest getMessageDigest(String algorithm) {
+        try {
+            return MessageDigest.getInstance(algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("Failed to obtain " + algorithm + " MessageDigest", e);
+        }
+    }
+
+    private static byte[] digest(String algorithm, byte[] data, int offset, int length) {
+        MessageDigest md = getMessageDigest(algorithm);
+        md.update(data, offset, length);
+        return md.digest();
+    }
+
+    private static byte[] digest(String algorithm, byte[] data) {
+        return getMessageDigest(algorithm).digest(data);
+    }
+
+    private static class NamedDigest {
+        private final String jcaDigestAlgorithm;
+        private final byte[] digest;
+
+        private NamedDigest(String jcaDigestAlgorithm, byte[] digest) {
+            this.jcaDigestAlgorithm = jcaDigestAlgorithm;
+            this.digest = digest;
+        }
+    }
+
+    public static class Result {
+
+        /** Whether the APK's JAR signature verifies. */
+        public boolean verified;
+
+        /** List of APK's signers. These signers are used by Android. */
+        public final List<SignerInfo> signers = new ArrayList<>();
+
+        /**
+         * Signers encountered in the APK but not included in the set of the APK's signers. These
+         * signers are ignored by Android.
+         */
+        public final List<SignerInfo> ignoredSigners = new ArrayList<>();
+
+        private final List<IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+        private boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            for (SignerInfo signer : signers) {
+                if (signer.containsErrors()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void addError(Issue msg, Object... parameters) {
+            mErrors.add(new IssueWithParams(msg, parameters));
+        }
+
+        private void addWarning(Issue msg, Object... parameters) {
+            mWarnings.add(new IssueWithParams(msg, parameters));
+        }
+
+        public List<IssueWithParams> getErrors() {
+            return mErrors;
+        }
+
+        public List<IssueWithParams> getWarnings() {
+            return mWarnings;
+        }
+
+        public static class SignerInfo {
+            public final String name;
+            public final String signatureFileName;
+            public final String signatureBlockFileName;
+            public final List<X509Certificate> certChain = new ArrayList<>();
+
+            private final List<IssueWithParams> mWarnings = new ArrayList<>();
+            private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+            private SignerInfo(
+                    String name, String signatureBlockFileName, String signatureFileName) {
+                this.name = name;
+                this.signatureBlockFileName = signatureBlockFileName;
+                this.signatureFileName = signatureFileName;
+            }
+
+            private boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            private void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+
+            private void addWarning(Issue msg, Object... parameters) {
+                mWarnings.add(new IssueWithParams(msg, parameters));
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java
new file mode 100644
index 0000000..793300c
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2016 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.apksigner.core.internal.jar;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+
+/**
+ * JAR manifest and signature file parser.
+ *
+ * <p>These files consist of a main section followed by individual sections. Individual sections
+ * are named, their names referring to JAR entries.
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public class ManifestParser {
+
+    private final byte[] mManifest;
+    private int mOffset;
+    private int mEndOffset;
+
+    private String mBufferedLine;
+
+    /**
+     * Constructs a new {@code ManifestParser} with the provided input.
+     */
+    public ManifestParser(byte[] data) {
+        this(data, 0, data.length);
+    }
+
+    /**
+     * Constructs a new {@code ManifestParser} with the provided input.
+     */
+    public ManifestParser(byte[] data, int offset, int length) {
+        mManifest = data;
+        mOffset = offset;
+        mEndOffset = offset + length;
+    }
+
+    /**
+     * Returns the remaining sections of this file.
+     */
+    public List<Section> readAllSections() {
+        List<Section> sections = new ArrayList<>();
+        Section section;
+        while ((section = readSection()) != null) {
+            sections.add(section);
+        }
+        return sections;
+    }
+
+    /**
+     * Returns the next section from this file or {@code null} if end of file has been reached.
+     */
+    public Section readSection() {
+        // Locate the first non-empty line
+        int sectionStartOffset;
+        String attr;
+        do {
+            sectionStartOffset = mOffset;
+            attr = readAttribute();
+            if (attr == null) {
+                return null;
+            }
+        } while (attr.length() == 0);
+        List<Attribute> attrs = new ArrayList<>();
+        attrs.add(parseAttr(attr));
+
+        // Read attributes until end of section reached
+        while (true) {
+            attr = readAttribute();
+            if ((attr == null) || (attr.length() == 0)) {
+                // End of section
+                break;
+            }
+            attrs.add(parseAttr(attr));
+        }
+
+        int sectionEndOffset = mOffset;
+        int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
+
+        return new Section(sectionStartOffset, sectionSizeBytes, attrs);
+    }
+
+    private static Attribute parseAttr(String attr) {
+        int delimiterIndex = attr.indexOf(':');
+        if (delimiterIndex == -1) {
+            return new Attribute(attr.trim(), "");
+        } else {
+            return new Attribute(
+                    attr.substring(0, delimiterIndex).trim(),
+                    attr.substring(delimiterIndex + 1).trim());
+        }
+    }
+
+    /**
+     * Returns the next attribute or empty {@code String} if end of section has been reached or
+     * {@code null} if end of input has been reached.
+     */
+    private String readAttribute() {
+        // Check whether end of section was reached during previous invocation
+        if ((mBufferedLine != null) && (mBufferedLine.length() == 0)) {
+            mBufferedLine = null;
+            return "";
+        }
+
+        // Read the next line
+        String line = readLine();
+        if (line == null) {
+            // End of input
+            if (mBufferedLine != null) {
+                String result = mBufferedLine;
+                mBufferedLine = null;
+                return result;
+            }
+            return null;
+        }
+
+        // Consume the read line
+        if (line.length() == 0) {
+            // End of section
+            if (mBufferedLine != null) {
+                String result = mBufferedLine;
+                mBufferedLine = "";
+                return result;
+            }
+            return "";
+        }
+        StringBuilder attrLine;
+        if (mBufferedLine == null) {
+            attrLine = new StringBuilder(line);
+        } else {
+            if (!line.startsWith(" ")) {
+                // The most common case: buffered line is a full attribute
+                String result = mBufferedLine;
+                mBufferedLine = line;
+                return result;
+            }
+            attrLine = new StringBuilder(mBufferedLine);
+            mBufferedLine = null;
+            attrLine.append(line.substring(1));
+        }
+
+        // Everything's buffered in attrLine now. mBufferedLine is null
+
+        // Read more lines
+        while (true) {
+            line = readLine();
+            if (line == null) {
+                // End of input
+                return attrLine.toString();
+            } else if (line.length() == 0) {
+                // End of section
+                mBufferedLine = ""; // make this method return "end of section" next time
+                return attrLine.toString();
+            }
+            if (line.startsWith(" ")) {
+                // Continuation line
+                attrLine.append(line.substring(1));
+            } else {
+                // Next attribute
+                mBufferedLine = line;
+                return attrLine.toString();
+            }
+        }
+    }
+
+    /**
+     * Returns the next line (without line delimiter characters) or {@code null} if end of input has
+     * been reached.
+     */
+    private String readLine() {
+        if (mOffset >= mEndOffset) {
+            return null;
+        }
+        int startOffset = mOffset;
+        int newlineStartOffset = -1;
+        int newlineEndOffset = -1;
+        for (int i = startOffset; i < mEndOffset; i++) {
+            byte b = mManifest[i];
+            if (b == '\r') {
+                newlineStartOffset = i;
+                int nextIndex = i + 1;
+                if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
+                    newlineEndOffset = nextIndex + 1;
+                    break;
+                }
+                newlineEndOffset = nextIndex;
+                break;
+            } else if (b == '\n') {
+                newlineStartOffset = i;
+                newlineEndOffset = i + 1;
+                break;
+            }
+        }
+        if (newlineStartOffset == -1) {
+            newlineStartOffset = mEndOffset;
+            newlineEndOffset = mEndOffset;
+        }
+        mOffset = newlineEndOffset;
+
+        int lineLengthBytes = newlineStartOffset - startOffset;
+        if (lineLengthBytes == 0) {
+            return "";
+        }
+        return new String(mManifest, startOffset, lineLengthBytes, StandardCharsets.UTF_8);
+    }
+
+
+    /**
+     * Attribute.
+     */
+    public static class Attribute {
+        private final String mName;
+        private final String mValue;
+
+        /**
+         * Constructs a new {@code Attribute} with the provided name and value.
+         */
+        public Attribute(String name, String value) {
+            mName = name;
+            mValue = value;
+        }
+
+        /**
+         * Returns this attribute's name.
+         */
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns this attribute's value.
+         */
+        public String getValue() {
+            return mValue;
+        }
+    }
+
+    /**
+     * Section.
+     */
+    public static class Section {
+        private final int mStartOffset;
+        private final int mSizeBytes;
+        private final String mName;
+        private final List<Attribute> mAttributes;
+
+        /**
+         * Constructs a new {@code Section}.
+         *
+         * @param startOffset start offset (in bytes) of the section in the input file
+         * @param sizeBytes size (in bytes) of the section in the input file
+         * @param attrs attributes contained in the section
+         */
+        public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
+            mStartOffset = startOffset;
+            mSizeBytes = sizeBytes;
+            String sectionName = null;
+            if (!attrs.isEmpty()) {
+                Attribute firstAttr = attrs.get(0);
+                if ("Name".equalsIgnoreCase(firstAttr.getName())) {
+                    sectionName = firstAttr.getValue();
+                }
+            }
+            mName = sectionName;
+            mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Returns the offset (in bytes) at which this section starts in the input.
+         */
+        public int getStartOffset() {
+            return mStartOffset;
+        }
+
+        /**
+         * Returns the size (in bytes) of this section in the input.
+         */
+        public int getSizeBytes() {
+            return mSizeBytes;
+        }
+
+        /**
+         * Returns this section's attributes, in the order in which they appear in the input.
+         */
+        public List<Attribute> getAttributes() {
+            return mAttributes;
+        }
+
+        /**
+         * Returns the value of the specified attribute in this section or {@code null} if this
+         * section does not contain a matching attribute.
+         */
+        public String getAttributeValue(Attributes.Name name) {
+            return getAttributeValue(name.toString());
+        }
+
+        /**
+         * Returns the value of the specified attribute in this section or {@code null} if this
+         * section does not contain a matching attribute.
+         *
+         * @param name name of the attribute. Attribute names are case-insensitive.
+         */
+        public String getAttributeValue(String name) {
+            for (Attribute attr : mAttributes) {
+                if (attr.getName().equalsIgnoreCase(name)) {
+                    return attr.getValue();
+                }
+            }
+            return null;
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java
index 449953a..13b1aaf 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java
@@ -18,6 +18,7 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
@@ -26,6 +27,8 @@
 
 /**
  * Producer of {@code META-INF/MANIFEST.MF} file.
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
  */
 public abstract class ManifestWriter {
 
@@ -79,7 +82,7 @@
     }
 
     private static void writeLine(OutputStream  out, String line) throws IOException {
-        byte[] lineBytes = line.getBytes("UTF-8");
+        byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
         int offset = 0;
         int remaining = lineBytes.length;
         boolean firstLine = true;
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java
index 9cd25f3..94ae280 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java
@@ -23,6 +23,8 @@
 
 /**
  * Producer of JAR signature file ({@code *.SF}).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
  */
 public abstract class SignatureFileWriter {
     private SignatureFileWriter() {}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java
new file mode 100644
index 0000000..f7fb7cd
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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.apksigner.core.internal.util;
+
+/**
+ * Android SDK version / API Level constants.
+ */
+public abstract class AndroidSdkVersion {
+
+    /** Hidden constructor to prevent instantiation. */
+    private AndroidSdkVersion() {}
+
+    /** Android 4.3. The revenge of the beans. */
+    public static final int JELLY_BEAN_MR2 = 18;
+
+    /** Android 5.0. A flat one with beautiful shadows. But still tasty. */
+    public static final int LOLLIPOP = 21;
+
+    // TODO: Update Javadoc / constant name once N is assigned a proper name / version code.
+    /** Android N. */
+    public static final int N = 24;
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
new file mode 100644
index 0000000..6a5b94c
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2016 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.apksigner.core.internal.zip;
+
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
+
+/**
+ * ZIP Central Directory (CD) Record.
+ */
+public class CentralDirectoryRecord {
+
+    /**
+     * Comparator which compares records by the offset of the corresponding Local File Header in the
+     * archive.
+     */
+    public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR =
+            new ByLocalFileHeaderOffsetComparator();
+
+    private static final int RECORD_SIGNATURE = 0x02014b50;
+    private static final int HEADER_SIZE_BYTES = 46;
+
+    private static final int GP_FLAGS_OFFSET = 8;
+    private static final int COMPRESSION_METHOD_OFFSET = 10;
+    private static final int CRC32_OFFSET = 16;
+    private static final int COMPRESSED_SIZE_OFFSET = 20;
+    private static final int UNCOMPRESSED_SIZE_OFFSET = 24;
+    private static final int NAME_LENGTH_OFFSET = 28;
+    private static final int EXTRA_LENGTH_OFFSET = 30;
+    private static final int COMMENT_LENGTH_OFFSET = 32;
+    private static final int LOCAL_FILE_HEADER_OFFSET = 42;
+    private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
+
+    private final short mGpFlags;
+    private final short mCompressionMethod;
+    private final long mCrc32;
+    private final long mCompressedSize;
+    private final long mUncompressedSize;
+    private final long mLocalFileHeaderOffset;
+    private final String mName;
+
+    private CentralDirectoryRecord(
+            short gpFlags,
+            short compressionMethod,
+            long crc32,
+            long compressedSize,
+            long uncompressedSize,
+            long localFileHeaderOffset,
+            String name) {
+        mGpFlags = gpFlags;
+        mCompressionMethod = compressionMethod;
+        mCrc32 = crc32;
+        mCompressedSize = compressedSize;
+        mUncompressedSize = uncompressedSize;
+        mLocalFileHeaderOffset = localFileHeaderOffset;
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public short getGpFlags() {
+        return mGpFlags;
+    }
+
+    public short getCompressionMethod() {
+        return mCompressionMethod;
+    }
+
+    public long getCrc32() {
+        return mCrc32;
+    }
+
+    public long getCompressedSize() {
+        return mCompressedSize;
+    }
+
+    public long getUncompressedSize() {
+        return mUncompressedSize;
+    }
+
+    public long getLocalFileHeaderOffset() {
+        return mLocalFileHeaderOffset;
+    }
+
+    /**
+     * Returns the Central Directory Record starting at the current position of the provided buffer
+     * and advances the buffer's position immediately past the end of the record.
+     */
+    public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException {
+        ZipUtils.assertByteOrderLittleEndian(buf);
+        if (buf.remaining() < HEADER_SIZE_BYTES) {
+            throw new ZipFormatException(
+                    "Input too short. Need at least: " + HEADER_SIZE_BYTES
+                            + " bytes, available: " + buf.remaining() + " bytes",
+                    new BufferUnderflowException());
+        }
+        int bufPosition = buf.position();
+        int recordSignature = buf.getInt(bufPosition);
+        if (recordSignature != RECORD_SIGNATURE) {
+            throw new ZipFormatException(
+                    "Not a Central Directory record. Signature: 0x"
+                            + Long.toHexString(recordSignature & 0xffffffffL));
+        }
+        short gpFlags = buf.getShort(bufPosition + GP_FLAGS_OFFSET);
+        short compressionMethod = buf.getShort(bufPosition + COMPRESSION_METHOD_OFFSET);
+        long crc32 = ZipUtils.getUnsignedInt32(buf, bufPosition + CRC32_OFFSET);
+        long compressedSize = ZipUtils.getUnsignedInt32(buf, bufPosition + COMPRESSED_SIZE_OFFSET);
+        long uncompressedSize =
+                ZipUtils.getUnsignedInt32(buf,  bufPosition + UNCOMPRESSED_SIZE_OFFSET);
+        int nameSize = ZipUtils.getUnsignedInt16(buf, bufPosition + NAME_LENGTH_OFFSET);
+        int extraSize = ZipUtils.getUnsignedInt16(buf, bufPosition + EXTRA_LENGTH_OFFSET);
+        int commentSize = ZipUtils.getUnsignedInt16(buf, bufPosition + COMMENT_LENGTH_OFFSET);
+        long localFileHeaderOffset =
+                ZipUtils.getUnsignedInt32(buf, bufPosition + LOCAL_FILE_HEADER_OFFSET);
+        int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
+        if (recordSize > buf.remaining()) {
+            throw new ZipFormatException(
+                    "Input too short. Need: " + recordSize + " bytes, available: "
+                            + buf.remaining() + " bytes",
+                    new BufferUnderflowException());
+        }
+        String name = getName(buf, bufPosition + NAME_OFFSET, nameSize);
+        buf.position(bufPosition + recordSize);
+        return new CentralDirectoryRecord(
+                gpFlags,
+                compressionMethod,
+                crc32,
+                compressedSize,
+                uncompressedSize,
+                localFileHeaderOffset,
+                name);
+    }
+
+    static String getName(ByteBuffer record, int position, int nameLengthBytes) {
+        byte[] nameBytes;
+        int nameBytesOffset;
+        if (record.hasArray()) {
+            nameBytes = record.array();
+            nameBytesOffset = record.arrayOffset() + position;
+        } else {
+            nameBytes = new byte[nameLengthBytes];
+            nameBytesOffset = 0;
+            int originalPosition = record.position();
+            try {
+                record.position(position);
+                record.get(nameBytes);
+            } finally {
+                record.position(originalPosition);
+            }
+        }
+        return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8);
+    }
+
+    private static class ByLocalFileHeaderOffsetComparator
+            implements Comparator<CentralDirectoryRecord> {
+        @Override
+        public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) {
+            long offset1 = r1.getLocalFileHeaderOffset();
+            long offset2 = r2.getLocalFileHeaderOffset();
+            if (offset1 > offset2) {
+                return 1;
+            } else if (offset1 < offset2) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
new file mode 100644
index 0000000..99a606b
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2016 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.apksigner.core.internal.zip;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+/**
+ * ZIP Local File Header.
+ */
+public class LocalFileHeader {
+    private static final int RECORD_SIGNATURE = 0x04034b50;
+    private static final int HEADER_SIZE_BYTES = 30;
+
+    private static final int GP_FLAGS_OFFSET = 6;
+    private static final int COMPRESSION_METHOD_OFFSET = 8;
+    private static final int CRC32_OFFSET = 14;
+    private static final int COMPRESSED_SIZE_OFFSET = 18;
+    private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
+    private static final int NAME_LENGTH_OFFSET = 26;
+    private static final int EXTRA_LENGTH_OFFSET = 28;
+    private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
+
+    private static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
+
+    private LocalFileHeader() {}
+
+    /**
+     * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
+     */
+    public static byte[] getUncompressedData(
+            DataSource source,
+            long sourceOffsetInArchive,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffsetInArchive) throws ZipFormatException, IOException {
+        if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
+            throw new IOException(
+                    cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
+        }
+        byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
+        ByteBuffer resultBuf = ByteBuffer.wrap(result);
+        ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
+        sendUncompressedData(
+                source,
+                sourceOffsetInArchive,
+                cdRecord,
+                cdStartOffsetInArchive,
+                resultSink);
+        if (resultBuf.hasRemaining()) {
+            throw new ZipFormatException(
+                    "Data of " + cdRecord.getName() + " shorter than specified in Central Directory"
+                            + ". Expected: " + result.length + " bytes,  read: "
+                            + resultBuf.position() + " bytes");
+        }
+        return result;
+    }
+
+    /**
+     * Sends the uncompressed data pointed to by the provided ZIP Central Directory (CD) record into
+     * the provided data sink.
+     */
+    public static void sendUncompressedData(
+            DataSource source,
+            long sourceOffsetInArchive,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffsetInArchive,
+            DataSink sink) throws ZipFormatException, IOException {
+
+        // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
+        // exhibited when reading an APK for the purposes of verifying its signatures.
+
+        String entryName = cdRecord.getName();
+        byte[] cdNameBytes = entryName.getBytes(StandardCharsets.UTF_8);
+        int headerSizeWithName = HEADER_SIZE_BYTES + cdNameBytes.length;
+        long localFileHeaderOffsetInArchive = cdRecord.getLocalFileHeaderOffset();
+        long headerEndInArchive = localFileHeaderOffsetInArchive + headerSizeWithName;
+        if (headerEndInArchive >= cdStartOffsetInArchive) {
+            throw new ZipFormatException(
+                    "Local File Header of " + entryName + " extends beyond start of Central"
+                            + " Directory. LFH end: " + headerEndInArchive
+                            + ", CD start: " + cdStartOffsetInArchive);
+        }
+        ByteBuffer header;
+        try {
+            header =
+                    source.getByteBuffer(
+                            localFileHeaderOffsetInArchive - sourceOffsetInArchive,
+                            headerSizeWithName);
+        } catch (IOException e) {
+            throw new IOException("Failed to read Local File Header of " + entryName, e);
+        }
+        header.order(ByteOrder.LITTLE_ENDIAN);
+
+        int recordSignature = header.getInt(0);
+        if (recordSignature != RECORD_SIGNATURE) {
+            throw new ZipFormatException(
+                    "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
+                            + Long.toHexString(recordSignature & 0xffffffffL));
+        }
+        short gpFlags = header.getShort(GP_FLAGS_OFFSET);
+        if ((gpFlags & GP_FLAG_DATA_DESCRIPTOR_USED) == 0) {
+            long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
+            if (crc32 != cdRecord.getCrc32()) {
+                throw new ZipFormatException(
+                        "CRC-32 mismatch between Local File Header and Central Directory for entry "
+                                + entryName + ". LFH: " + crc32 + ", CD: " + cdRecord.getCrc32());
+            }
+            long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
+            if (compressedSize != cdRecord.getCompressedSize()) {
+                throw new ZipFormatException(
+                        "Compressed size mismatch between Local File Header and Central Directory"
+                                + " for entry " + entryName + ". LFH: " + compressedSize
+                                + ", CD: " + cdRecord.getCompressedSize());
+            }
+            long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
+            if (uncompressedSize != cdRecord.getUncompressedSize()) {
+                throw new ZipFormatException(
+                        "Uncompressed size mismatch between Local File Header and Central Directory"
+                                + " for entry " + entryName + ". LFH: " + uncompressedSize
+                                + ", CD: " + cdRecord.getUncompressedSize());
+            }
+        }
+        int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
+        if (nameLength > cdNameBytes.length) {
+            throw new ZipFormatException(
+                    "Name mismatch between Local File Header and Central Directory for entry"
+                            + entryName + ". LFH: " + nameLength
+                            + " bytes, CD: " + cdNameBytes.length + " bytes");
+        }
+        String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
+        if (!entryName.equals(name)) {
+            throw new ZipFormatException(
+                    "Name mismatch between Local File Header and Central Directory. LFH: \""
+                            + name + "\", CD: \"" + entryName + "\"");
+        }
+        int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
+
+        short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET);
+        boolean compressed;
+        switch (compressionMethod) {
+            case ZipUtils.COMPRESSION_METHOD_STORED:
+                compressed = false;
+                break;
+            case ZipUtils.COMPRESSION_METHOD_DEFLATED:
+                compressed = true;
+                break;
+            default:
+                throw new ZipFormatException(
+                        "Unsupported compression method of entry " + entryName
+                                + ": " + (compressionMethod & 0xffff));
+        }
+
+        long dataStartOffsetInArchive =
+                localFileHeaderOffsetInArchive + HEADER_SIZE_BYTES + nameLength + extraLength;
+        long dataSize;
+        if (compressed) {
+            dataSize = cdRecord.getCompressedSize();
+        } else {
+            dataSize = cdRecord.getUncompressedSize();
+        }
+        long dataEndOffsetInArchive = dataStartOffsetInArchive + dataSize;
+        if (dataEndOffsetInArchive > cdStartOffsetInArchive) {
+            throw new ZipFormatException(
+                    "Local File Header data of " + entryName + " extends beyond Central Directory"
+                            + ". LFH data start: " + dataStartOffsetInArchive
+                            + ", LFH data end: " + dataEndOffsetInArchive
+                            + ", CD start: " + cdStartOffsetInArchive);
+        }
+
+        long dataOffsetInSource = dataStartOffsetInArchive - sourceOffsetInArchive;
+        try {
+            if (compressed) {
+                try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
+                    source.feed(dataOffsetInSource, dataSize, inflateAdapter);
+                }
+            } else {
+                source.feed(dataOffsetInSource, dataSize, sink);
+            }
+        } catch (IOException e) {
+            throw new IOException(
+                    "Failed to read data of " + ((compressed) ? "compressed" : "uncompressed")
+                        + " entry " + entryName,
+                    e);
+        }
+        // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
+        // thus don't check either.
+    }
+
+    private static class InflateSinkAdapter implements DataSink, Closeable {
+        private final DataSink mDelegate;
+
+        private Inflater mInflater = new Inflater(true);
+        private byte[] mOutputBuffer;
+        private byte[] mInputBuffer;
+        private boolean mClosed;
+
+        private InflateSinkAdapter(DataSink delegate) {
+            mDelegate = delegate;
+        }
+
+        @Override
+        public void consume(byte[] buf, int offset, int length) throws IOException {
+            checkNotClosed();
+            mInflater.setInput(buf, offset, length);
+            if (mOutputBuffer == null) {
+                mOutputBuffer = new byte[65536];
+            }
+            while (!mInflater.finished()) {
+                int outputChunkSize;
+                try {
+                    outputChunkSize = mInflater.inflate(mOutputBuffer);
+                } catch (DataFormatException e) {
+                    throw new IOException("Failed to inflate data", e);
+                }
+                if (outputChunkSize == 0) {
+                    return;
+                }
+                // mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
+                mDelegate.consume(ByteBuffer.wrap(mOutputBuffer, 0, outputChunkSize));
+            }
+        }
+
+        @Override
+        public void consume(ByteBuffer buf) throws IOException {
+            checkNotClosed();
+            if (buf.hasArray()) {
+                consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+                buf.position(buf.limit());
+            } else {
+                if (mInputBuffer == null) {
+                    mInputBuffer = new byte[65536];
+                }
+                while (buf.hasRemaining()) {
+                    int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
+                    buf.get(mInputBuffer, 0, chunkSize);
+                    consume(mInputBuffer, 0, chunkSize);
+                }
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            mClosed = true;
+            mInputBuffer = null;
+            mOutputBuffer = null;
+            if (mInflater != null) {
+                mInflater.end();
+                mInflater = null;
+            }
+        }
+
+        private void checkNotClosed() {
+            if (mClosed) {
+                throw new IllegalStateException("Closed");
+            }
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
index 5e724a2..118585a 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -255,13 +255,13 @@
         return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
     }
 
-    private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
+    static void assertByteOrderLittleEndian(ByteBuffer buffer) {
         if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
             throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
         }
     }
 
-    private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
+    public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
         return buffer.getShort(offset) & 0xffff;
     }
 
@@ -272,7 +272,7 @@
         buffer.putInt(offset, (int) value);
     }
 
-    private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
+    static long getUnsignedInt32(ByteBuffer buffer, int offset) {
         return buffer.getInt(offset) & 0xffffffffL;
     }
 }
\ No newline at end of file
diff --git a/tools/ijar/Android.bp b/tools/ijar/Android.bp
index f7e9a28..a244a2d 100644
--- a/tools/ijar/Android.bp
+++ b/tools/ijar/Android.bp
@@ -15,6 +15,4 @@
     ],
     host_ldlibs: ["-lz"],
     name: "ijar",
-    // libc++ is not supported for TARGET_BUILD_APPS builds
-    stl: "libstdc++",
 }
diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java
index 5ba0666..f1f340d 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -57,6 +57,7 @@
 import java.lang.reflect.Constructor;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.Key;
 import java.security.KeyFactory;
@@ -717,7 +718,7 @@
         // archive comment, so that tools that display the comment
         // (hopefully) show something sensible.
         // TODO: anything more useful we can put in this message?
-        byte[] message = "signed by SignApk".getBytes("UTF-8");
+        byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8);
         temp.write(message);
         temp.write(0);