Merge "Add variable PRODUCT_SHIPPING_API_LEVEL and make files with the new read only product property ro.product.first_api_level."
diff --git a/core/base_rules.mk b/core/base_rules.mk
index 30b6c1c..a84ea27 100644
--- a/core/base_rules.mk
+++ b/core/base_rules.mk
@@ -156,6 +156,8 @@
     partition_tag := _OEM
   else ifeq (true,$(LOCAL_ODM_MODULE))
     partition_tag := _ODM
+  else ifeq (NATIVE_TESTS,$(LOCAL_MODULE_CLASS))
+    partition_tag := _DATA
   else
     # The definition of should-install-to-system will be different depending
     # on which goal (e.g., sdk or just droid) is being built.
@@ -457,7 +459,7 @@
 ## umbrella targets used to verify builds
 ###########################################################
 j_or_n :=
-ifneq (,$(filter EXECUTABLES SHARED_LIBRARIES STATIC_LIBRARIES,$(LOCAL_MODULE_CLASS)))
+ifneq (,$(filter EXECUTABLES SHARED_LIBRARIES STATIC_LIBRARIES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)))
 j_or_n := native
 else
 ifneq (,$(filter JAVA_LIBRARIES APPS,$(LOCAL_MODULE_CLASS)))
diff --git a/core/binary.mk b/core/binary.mk
index f13689a..c3bf451 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -186,7 +186,7 @@
 # all code is position independent, and then those warnings get promoted to
 # errors.
 ifneq ($($(my_prefix)OS),windows)
-ifeq ($(LOCAL_MODULE_CLASS),EXECUTABLES)
+ifneq ($(filter EXECUTABLES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)),)
 my_cflags += -fpie
 else
 my_cflags += -fPIC
@@ -1133,6 +1133,34 @@
 endif
 
 
+####################################################
+## For NDK-built libraries, move LOCAL_SHARED_LIBRARY
+## references to my_ldlibs, so that we use the NDK
+## prebuilt library and headers for linking.
+####################################################
+ifndef LOCAL_IS_HOST_MODULE
+my_allowed_ldlibs :=
+ifdef LOCAL_SDK_VERSION
+  my_ndk_shared_libraries := $(filter $(addprefix lib,$(NDK_PREBUILT_SHARED_LIBRARIES)),$(my_shared_libraries))
+  my_shared_libraries := $(filter-out $(my_ndk_shared_libraries),$(my_shared_libraries))
+  my_ldlibs += $(patsubst lib%,-l%,$(my_ndk_shared_libraries))
+  my_ndk_shared_libraries :=
+  my_allowed_ldlibs := $(addprefix -l,$(NDK_PREBUILT_SHARED_LIBRARIES))
+endif
+
+# Sort ldlibs and ldflags between -l and other linker flags
+# We'll do this again later, since there are still changes happening, but that's fine.
+my_ldlib_flags := $(my_ldflags) $(my_ldlibs)
+my_ldlibs := $(filter -l%,$(my_ldlib_flags))
+my_ldflags := $(filter-out -l%,$(my_ldlib_flags))
+my_ldlib_flags :=
+
+# Move other ldlibs back to shared libraries
+my_shared_libraries += $(patsubst -l%,lib%,$(filter-out $(my_allowed_ldlibs),$(my_ldlibs)))
+my_ldlibs := $(filter $(my_allowed_ldlibs),$(my_ldlibs))
+endif
+
+
 ##########################################################
 ## Set up installed module dependency
 ## We cannot compute the full path of the LOCAL_SHARED_LIBRARIES for
@@ -1228,6 +1256,11 @@
   my_c_includes += $(JNI_H_INCLUDE)
 endif
 
+my_outside_includes := $(filter-out $(OUT_DIR)/%,$(filter /%,$(my_c_includes)))
+ifneq ($(my_outside_includes),)
+$(error $(LOCAL_MODULE_MAKEFILE): $(LOCAL_MODULE): C_INCLUDES must be under the source or output directories: $(my_outside_includes))
+endif
+
 # all_objects includes gen_o_objects which were part of LOCAL_GENERATED_SOURCES;
 # use normal_objects here to avoid creating circular dependencies. This assumes
 # that custom build rules which generate .o files don't consume other generated
@@ -1426,6 +1459,24 @@
   endif
 endif
 
+# Move -l* entries from ldflags to ldlibs, and everything else to ldflags
+my_ldlib_flags := $(my_ldflags) $(my_ldlibs)
+my_ldlibs := $(filter -l%,$(my_ldlib_flags))
+my_ldflags := $(filter-out -l%,$(my_ldlib_flags))
+
+# One last verification check for ldlibs
+ifndef LOCAL_IS_HOST_MODULE
+my_allowed_ldlibs :=
+ifdef LOCAL_SDK_VERSION
+  my_allowed_ldlibs := $(addprefix -l,$(NDK_PREBUILT_SHARED_LIBRARIES))
+endif
+
+my_bad_ldlibs := $(filter-out $(my_allowed_ldlibs),$(my_ldlibs))
+ifneq ($(my_bad_ldlibs),)
+  $(error $(LOCAL_MODULE_MAKEFILE): $(LOCAL_MODULE): Bad LOCAL_LDLIBS entries: $(my_bad_ldlibs))
+endif
+endif
+
 $(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_YACCFLAGS := $(LOCAL_YACCFLAGS)
 $(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_ASFLAGS := $(my_asflags)
 $(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_CONLYFLAGS := $(my_conlyflags)
diff --git a/core/clang/config.mk b/core/clang/config.mk
index a269839..4685514 100644
--- a/core/clang/config.mk
+++ b/core/clang/config.mk
@@ -3,6 +3,12 @@
 LLVM_PREBUILTS_PATH := $(LLVM_PREBUILTS_BASE)/$(BUILD_OS)-x86/$(LLVM_PREBUILTS_VERSION)/bin
 LLVM_RTLIB_PATH := $(LLVM_PREBUILTS_PATH)/../lib64/clang/$(LLVM_RELEASE_VERSION)/lib/linux/
 
+# These will come from Soong, drop the environment versions
+unexport CLANG
+unexport CLANG_CXX
+unexport CCC_CC
+unexport CCC_CXX
+
 CLANG_TBLGEN := $(BUILD_OUT_EXECUTABLES)/clang-tblgen$(BUILD_EXECUTABLE_SUFFIX)
 LLVM_TBLGEN := $(BUILD_OUT_EXECUTABLES)/llvm-tblgen$(BUILD_EXECUTABLE_SUFFIX)
 
diff --git a/core/combo/HOST_darwin-x86.mk b/core/combo/HOST_darwin-x86.mk
index 9e1ecc5..534c823 100644
--- a/core/combo/HOST_darwin-x86.mk
+++ b/core/combo/HOST_darwin-x86.mk
@@ -49,8 +49,8 @@
 
 define transform-host-o-to-executable-inner
 $(hide) $(PRIVATE_CXX) \
-        -Wl,-rpath,@loader_path/../$(notdir $($(PRIVATE_2ND_ARCH_VAR_PREFIX)HOST_OUT_SHARED_LIBRARIES)) \
-        -Wl,-rpath,@loader_path/$(notdir $($(PRIVATE_2ND_ARCH_VAR_PREFIX)HOST_OUT_SHARED_LIBRARIES)) \
+        $(foreach path,$(PRIVATE_RPATHS), \
+          -Wl,-rpath,@loader_path/$(path)) \
         -o $@ \
         -Wl,-headerpad_max_install_names \
         $($(PRIVATE_2ND_ARCH_VAR_PREFIX)HOST_GLOBAL_LD_DIRS) \
diff --git a/core/config.mk b/core/config.mk
index 727a5a8..6f693f5 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -781,6 +781,8 @@
 RSCOMPAT_32BIT_ONLY_API_LEVELS := 8 9 10 11 12 13 14 15 16 17 18 19 20
 RSCOMPAT_NO_USAGEIO_API_LEVELS := 8 9 10 11 12 13
 
+NDK_PREBUILT_SHARED_LIBRARIES := android c dl EGL GLESv1_CM GLESv2 GLESv3 jnigraphics log mediandk m OpenMAXAL OpenSLES stdc++ vulkan z
+
 ifeq ($(JAVA_NOT_REQUIRED),true)
 # Remove java and tools from our path so that we make sure nobody uses them.
 unexport ANDROID_JAVA_HOME
diff --git a/core/definitions.mk b/core/definitions.mk
index 7572070..4fc21d5 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -1877,8 +1877,8 @@
 	$(if $(filter true,$(NATIVE_COVERAGE)),$(PRIVATE_HOST_LIBPROFILE_RT)) \
 	$(call normalize-host-libraries,$(PRIVATE_ALL_SHARED_LIBRARIES)) \
 	-Wl,-rpath-link=$($(PRIVATE_2ND_ARCH_VAR_PREFIX)$(PRIVATE_PREFIX)OUT_INTERMEDIATE_LIBRARIES) \
-	-Wl,-rpath,\$$ORIGIN/../$(notdir $($(PRIVATE_2ND_ARCH_VAR_PREFIX)$(PRIVATE_PREFIX)OUT_SHARED_LIBRARIES)) \
-	-Wl,-rpath,\$$ORIGIN/$(notdir $($(PRIVATE_2ND_ARCH_VAR_PREFIX)$(PRIVATE_PREFIX)OUT_SHARED_LIBRARIES)) \
+	$(foreach path,$(PRIVATE_RPATHS), \
+	  -Wl,-rpath,\$$ORIGIN/$(path)) \
 	$($(PRIVATE_2ND_ARCH_VAR_PREFIX)$(PRIVATE_PREFIX)GLOBAL_LD_DIRS) \
 	$(if $(PRIVATE_NO_DEFAULT_COMPILER_FLAGS),, \
 		$(PRIVATE_HOST_GLOBAL_LDFLAGS) \
diff --git a/core/dynamic_binary.mk b/core/dynamic_binary.mk
index 58f76b0..579338e 100644
--- a/core/dynamic_binary.mk
+++ b/core/dynamic_binary.mk
@@ -58,7 +58,7 @@
 # Do not pack relocations for executables. Because packing results in
 # non-zero p_vaddr which causes kernel to load executables to lower
 # address (starting at 0x8000) http://b/20665974
-ifeq ($(LOCAL_MODULE_CLASS),EXECUTABLES)
+ifneq ($(filter EXECUTABLES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)),)
   my_pack_module_relocations := false
 endif
 
diff --git a/core/envsetup.mk b/core/envsetup.mk
index 4b48b27..0a7472a 100644
--- a/core/envsetup.mk
+++ b/core/envsetup.mk
@@ -247,9 +247,11 @@
 HOST_OUT_RENDERSCRIPT_BITCODE := $(HOST_OUT_SHARED_LIBRARIES)
 HOST_OUT_JAVA_LIBRARIES := $(HOST_OUT)/framework
 HOST_OUT_SDK_ADDON := $(HOST_OUT)/sdk_addon
+HOST_OUT_NATIVE_TESTS := $(HOST_OUT)/nativetest64
 
 HOST_CROSS_OUT_EXECUTABLES := $(HOST_CROSS_OUT)/bin
 HOST_CROSS_OUT_SHARED_LIBRARIES := $(HOST_CROSS_OUT)/lib
+HOST_CROSS_OUT_NATIVE_TESTS := $(HOST_CROSS_OUT)/nativetest
 
 HOST_OUT_INTERMEDIATES := $(HOST_OUT)/obj
 HOST_OUT_HEADERS := $(HOST_OUT_INTERMEDIATES)/include
@@ -276,6 +278,7 @@
 $(HOST_2ND_ARCH_VAR_PREFIX)HOST_OUT_SHARED_LIBRARIES := $(HOST_OUT)/lib
 $(HOST_2ND_ARCH_VAR_PREFIX)HOST_OUT_EXECUTABLES := $(HOST_OUT_EXECUTABLES)
 $(HOST_2ND_ARCH_VAR_PREFIX)HOST_OUT_JAVA_LIBRARIES := $(HOST_OUT_JAVA_LIBRARIES)
+$(HOST_2ND_ARCH_VAR_PREFIX)HOST_OUT_NATIVE_TESTS := $(HOST_OUT)/nativetest
 
 # The default host library path.
 # It always points to the path where we build libraries in the default bitness.
@@ -292,6 +295,7 @@
 $(HOST_CROSS_2ND_ARCH_VAR_PREFIX)HOST_CROSS_OUT_INTERMEDIATE_LIBRARIES := $($(HOST_CROSS_2ND_ARCH_VAR_PREFIX)HOST_CROSS_OUT_INTERMEDIATES)/lib
 $(HOST_CROSS_2ND_ARCH_VAR_PREFIX)HOST_CROSS_OUT_SHARED_LIBRARIES := $(HOST_CROSS_OUT)/lib64
 $(HOST_CROSS_2ND_ARCH_VAR_PREFIX)HOST_CROSS_OUT_EXECUTABLES := $(HOST_CROSS_OUT_EXECUTABLES)
+$(HOST_CROSS_2ND_ARCH_VAR_PREFIX)HOST_CROSS_OUT_NATIVE_TESTS := $(HOST_CROSS_OUT)/nativetest64
 
 TARGET_OUT_INTERMEDIATES := $(PRODUCT_OUT)/obj
 TARGET_OUT_HEADERS := $(TARGET_OUT_INTERMEDIATES)/include
@@ -467,7 +471,7 @@
 TARGET_INSTALLER_SYSTEM_OUT := $(TARGET_INSTALLER_OUT)/root/system
 
 COMMON_MODULE_CLASSES := TARGET-NOTICE_FILES HOST-NOTICE_FILES HOST-JAVA_LIBRARIES
-PER_ARCH_MODULE_CLASSES := SHARED_LIBRARIES STATIC_LIBRARIES EXECUTABLES GYP RENDERSCRIPT_BITCODE
+PER_ARCH_MODULE_CLASSES := SHARED_LIBRARIES STATIC_LIBRARIES EXECUTABLES GYP RENDERSCRIPT_BITCODE NATIVE_TESTS
 
 ifeq (,$(strip $(DIST_DIR)))
   DIST_DIR := $(OUT_DIR)/dist
diff --git a/core/executable.mk b/core/executable.mk
index 27c033d..8652077 100644
--- a/core/executable.mk
+++ b/core/executable.mk
@@ -30,12 +30,14 @@
 include $(BUILD_SYSTEM)/multilib.mk
 
 ifeq ($(my_module_multilib),both)
+ifneq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
 ifeq ($(LOCAL_MODULE_PATH_32)$(LOCAL_MODULE_STEM_32),)
 $(error $(LOCAL_PATH): LOCAL_MODULE_STEM_32 or LOCAL_MODULE_PATH_32 is required for LOCAL_MULTILIB := both for module $(LOCAL_MODULE))
 endif
 ifeq ($(LOCAL_MODULE_PATH_64)$(LOCAL_MODULE_STEM_64),)
 $(error $(LOCAL_PATH): LOCAL_MODULE_STEM_64 or LOCAL_MODULE_PATH_64 is required for LOCAL_MULTILIB := both for module $(LOCAL_MODULE))
 endif
+endif
 else #!LOCAL_MULTILIB == both
 LOCAL_NO_2ND_ARCH_MODULE_SUFFIX := true
 endif
diff --git a/core/host_executable.mk b/core/host_executable.mk
index 6f19bd1..0060c3e 100644
--- a/core/host_executable.mk
+++ b/core/host_executable.mk
@@ -19,12 +19,14 @@
 endif
 
 ifeq ($(my_module_multilib),both)
+ifneq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
 ifeq ($(LOCAL_MODULE_PATH_32)$(LOCAL_MODULE_STEM_32),)
 $(error $(LOCAL_PATH): LOCAL_MODULE_STEM_32 or LOCAL_MODULE_PATH_32 is required for LOCAL_MULTILIB := both for module $(LOCAL_MODULE))
 endif
 ifeq ($(LOCAL_MODULE_PATH_64)$(LOCAL_MODULE_STEM_64),)
 $(error $(LOCAL_PATH): LOCAL_MODULE_STEM_64 or LOCAL_MODULE_PATH_64 is required for LOCAL_MULTILIB := both for module $(LOCAL_MODULE))
 endif
+endif
 else #!LOCAL_MULTILIB == both
 LOCAL_NO_2ND_ARCH_MODULE_SUFFIX := true
 endif
diff --git a/core/host_executable_internal.mk b/core/host_executable_internal.mk
index b682ffd..19200fd 100644
--- a/core/host_executable_internal.mk
+++ b/core/host_executable_internal.mk
@@ -29,6 +29,14 @@
 my_host_libprofile_rt := $($(LOCAL_2ND_ARCH_VAR_PREFIX)$(my_prefix)LIBPROFILE_RT)
 $(LOCAL_BUILT_MODULE): PRIVATE_HOST_LIBPROFILE_RT := $(my_host_libprofile_rt)
 
+my_libdir := $(notdir $($(LOCAL_2ND_ARCH_VAR_PREFIX)$(my_prefix)OUT_SHARED_LIBRARIES))
+ifeq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
+$(LOCAL_BUILT_MODULE): PRIVATE_RPATHS := ../../$(my_libdir)
+else
+$(LOCAL_BUILT_MODULE): PRIVATE_RPATHS := ../$(my_libdir) $(my_libdir)
+endif
+my_libdir :=
+
 $(LOCAL_BUILT_MODULE): $(all_objects) $(all_libraries)
 	$(transform-host-o-to-executable)
 
diff --git a/core/host_native_test.mk b/core/host_native_test.mk
index 7cba1ae..c6d6f52 100644
--- a/core/host_native_test.mk
+++ b/core/host_native_test.mk
@@ -3,24 +3,24 @@
 ## Common flags for host native tests are added.
 ################################################
 
+ifdef LOCAL_MODULE_CLASS
+ifneq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
+$(error $(LOCAL_PATH): LOCAL_MODULE_CLASS must be NATIVE_TESTS with BUILD_HOST_NATIVE_TEST)
+endif
+endif
+
+LOCAL_MODULE_CLASS := NATIVE_TESTS
+
 include $(BUILD_SYSTEM)/host_test_internal.mk
 
-needs_symlink :=
 ifndef LOCAL_MULTILIB
-  ifndef LOCAL_32_BIT_ONLY
-    LOCAL_MULTILIB := both
+ifndef LOCAL_32_BIT_ONLY
+LOCAL_MULTILIB := both
+endif
+endif
 
-    ifeq (,$(LOCAL_MODULE_STEM_32)$(LOCAL_MODULE_STEM_64))
-      LOCAL_MODULE_STEM_32 := $(LOCAL_MODULE)32
-      LOCAL_MODULE_STEM_64 := $(LOCAL_MODULE)64
-      needs_symlink := true
-    endif
-  endif
+ifndef LOCAL_MODULE_RELATIVE_PATH
+LOCAL_MODULE_RELATIVE_PATH := $(LOCAL_MODULE)
 endif
 
 include $(BUILD_HOST_EXECUTABLE)
-
-ifdef needs_symlink
-include $(BUILD_SYSTEM)/executable_prefer_symlink.mk
-needs_symlink :=
-endif
diff --git a/core/host_shared_test_lib.mk b/core/host_shared_test_lib.mk
index 1eb9b26..2c2063d 100644
--- a/core/host_shared_test_lib.mk
+++ b/core/host_shared_test_lib.mk
@@ -3,6 +3,8 @@
 ## Common flags for host native tests are added.
 ##################################################
 
+$(error BUILD_HOST_SHARED_TEST_LIBRARY is obsolete)
+
 include $(BUILD_SYSTEM)/host_test_internal.mk
 
 include $(BUILD_HOST_SHARED_LIBRARY)
diff --git a/core/host_test_internal.mk b/core/host_test_internal.mk
index 6c52e64..70f011b 100644
--- a/core/host_test_internal.mk
+++ b/core/host_test_internal.mk
@@ -11,3 +11,15 @@
 LOCAL_CFLAGS += -DGTEST_HAS_STD_STRING -O0 -g
 
 LOCAL_STATIC_LIBRARIES += libgtest_main_host libgtest_host
+
+ifdef LOCAL_MODULE_PATH
+$(error $(LOCAL_PATH): Do not set LOCAL_MODULE_PATH when building test $(LOCAL_MODULE))
+endif
+
+ifdef LOCAL_MODULE_PATH_32
+$(error $(LOCAL_PATH): Do not set LOCAL_MODULE_PATH_32 when building test $(LOCAL_MODULE))
+endif
+
+ifdef LOCAL_MODULE_PATH_64
+$(error $(LOCAL_PATH): Do not set LOCAL_MODULE_PATH_64 when building test $(LOCAL_MODULE))
+endif
diff --git a/core/main.mk b/core/main.mk
index 2c97fcf..a2f80ef 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -630,7 +630,7 @@
       $(eval r := $(addprefix host_cross_,$(r))))\
     $(if $(ALL_MODULES.$(m).FOR_2ND_ARCH),\
       $(eval r_r := $(call get-32-bit-modules-if-we-can,$(r))),\
-      $(if $(filter EXECUTABLES SHARED_LIBRARIES,$(ALL_MODULES.$(m).CLASS)),\
+      $(if $(filter EXECUTABLES SHARED_LIBRARIES NATIVE_TESTS,$(ALL_MODULES.$(m).CLASS)),\
         $(eval r_r := $(r)),\
         $(eval r_r := $(r) $(call get-32-bit-modules,$(r)))\
        )\
diff --git a/core/native_test.mk b/core/native_test.mk
index 93b7e1a..d4b2a5b 100644
--- a/core/native_test.mk
+++ b/core/native_test.mk
@@ -3,6 +3,11 @@
 ## Common flags for native tests are added.
 ###########################################
 
+# TODO: enforce NATIVE_TESTS once current users are gone
+ifndef LOCAL_MODULE_CLASS
+LOCAL_MODULE_CLASS := NATIVE_TESTS
+endif
+
 include $(BUILD_SYSTEM)/target_test_internal.mk
 
 ifndef LOCAL_MULTILIB
@@ -11,4 +16,10 @@
 endif
 endif
 
+ifneq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
+$(warning $(LOCAL_PATH): $(LOCAL_MODULE): LOCAL_MODULE_CLASS should be NATIVE_TESTS with BUILD_NATIVE_TEST)
+LOCAL_MODULE_PATH_64 := $(TARGET_OUT_DATA_NATIVE_TESTS)/$(LOCAL_MODULE)
+LOCAL_MODULE_PATH_32 := $($(TARGET_2ND_ARCH_VAR_PREFIX)TARGET_OUT_DATA_NATIVE_TESTS)/$(LOCAL_MODULE)
+endif
+
 include $(BUILD_EXECUTABLE)
diff --git a/core/prebuilt.mk b/core/prebuilt.mk
index 7b83467..cbe2079 100644
--- a/core/prebuilt.mk
+++ b/core/prebuilt.mk
@@ -59,7 +59,7 @@
 
 ifdef LOCAL_IS_HOST_MODULE
 ifdef HOST_CROSS_OS
-ifneq (,$(filter EXECUTABLES STATIC_LIBRARIES SHARED_LIBRARIES,$(LOCAL_MODULE_CLASS)))
+ifneq (,$(filter EXECUTABLES STATIC_LIBRARIES SHARED_LIBRARIES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)))
 my_prefix := HOST_CROSS_
 LOCAL_HOST_PREFIX := $(my_prefix)
 include $(BUILD_SYSTEM)/module_arch_supported.mk
diff --git a/core/prebuilt_internal.mk b/core/prebuilt_internal.mk
index c4f1a7a..3f89d80 100644
--- a/core/prebuilt_internal.mk
+++ b/core/prebuilt_internal.mk
@@ -92,7 +92,7 @@
   ifdef LOCAL_IS_HOST_MODULE
     $(error Cannot strip/pack host module LOCAL_PATH=$(LOCAL_PATH))
   endif
-  ifeq ($(filter SHARED_LIBRARIES EXECUTABLES,$(LOCAL_MODULE_CLASS)),)
+  ifeq ($(filter SHARED_LIBRARIES EXECUTABLES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)),)
     $(error Can strip/pack only shared libraries or executables LOCAL_PATH=$(LOCAL_PATH))
   endif
   ifneq ($(LOCAL_PREBUILT_STRIP_COMMENTS),)
@@ -346,7 +346,7 @@
 else
 	$(transform-prebuilt-to-target)
 endif
-ifeq ($(LOCAL_MODULE_CLASS),EXECUTABLES)
+ifneq ($(filter EXECUTABLES NATIVE_TESTS,$(LOCAL_MODULE_CLASS)),)
 	$(hide) chmod +x $@
 endif
 endif # ! prebuilt_module_is_dex_javalib
diff --git a/core/shared_test_lib.mk b/core/shared_test_lib.mk
index fbfdc9c..f3b8807 100644
--- a/core/shared_test_lib.mk
+++ b/core/shared_test_lib.mk
@@ -3,6 +3,8 @@
 ## Common flags for native tests are added.
 #############################################
 
+$(error BUILD_SHARED_TEST_LIBRARY is obsolete)
+
 include $(BUILD_SYSTEM)/target_test_internal.mk
 
 include $(BUILD_SHARED_LIBRARY)
diff --git a/core/target_test_internal.mk b/core/target_test_internal.mk
index 9e25674..d321aaa 100644
--- a/core/target_test_internal.mk
+++ b/core/target_test_internal.mk
@@ -33,5 +33,8 @@
 $(error $(LOCAL_PATH): Do not set LOCAL_MODULE_PATH_64 when building test $(LOCAL_MODULE))
 endif
 
-LOCAL_MODULE_PATH_64 := $(TARGET_OUT_DATA_NATIVE_TESTS)/$(LOCAL_MODULE)
-LOCAL_MODULE_PATH_32 := $($(TARGET_2ND_ARCH_VAR_PREFIX)TARGET_OUT_DATA_NATIVE_TESTS)/$(LOCAL_MODULE)
+ifeq ($(LOCAL_MODULE_CLASS),NATIVE_TESTS)
+ifndef LOCAL_MODULE_RELATIVE_PATH
+LOCAL_MODULE_RELATIVE_PATH := $(LOCAL_MODULE)
+endif
+endif
diff --git a/libs/host/Android.bp b/libs/host/Android.bp
new file mode 100644
index 0000000..e5a5ecf
--- /dev/null
+++ b/libs/host/Android.bp
@@ -0,0 +1,20 @@
+cc_library_host_static {
+
+    srcs: ["CopyFile.c"],
+
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+
+    name: "libhost",
+    target: {
+        windows: {
+            enabled: true,
+        },
+    },
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+    stl: "none",
+
+}
diff --git a/libs/host/Android.mk b/libs/host/Android.mk
deleted file mode 100644
index 5e6a291..0000000
--- a/libs/host/Android.mk
+++ /dev/null
@@ -1,18 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES:= \
-    CopyFile.c
-
-LOCAL_CFLAGS := -Werror -Wall
-
-LOCAL_MODULE:= libhost
-LOCAL_MODULE_HOST_OS := darwin linux windows
-LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-LOCAL_CXX_STL := none
-
-include $(BUILD_HOST_STATIC_LIBRARY)
-
-# Include toolchain prebuilt modules if they exist.
--include $(TARGET_TOOLCHAIN_ROOT)/toolchain.mk
diff --git a/tools/acp/Android.bp b/tools/acp/Android.bp
new file mode 100644
index 0000000..faf2034
--- /dev/null
+++ b/tools/acp/Android.bp
@@ -0,0 +1,13 @@
+// Copyright 2005 The Android Open Source Project
+//
+// Custom version of cp.
+
+cc_binary_host {
+
+    srcs: ["acp.c"],
+
+    static_libs: ["libhost"],
+    name: "acp",
+    stl: "none",
+
+}
diff --git a/tools/acp/Android.mk b/tools/acp/Android.mk
deleted file mode 100644
index 56aac14..0000000
--- a/tools/acp/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2005 The Android Open Source Project
-#
-# Custom version of cp.
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    acp.c
-
-LOCAL_STATIC_LIBRARIES := libhost
-LOCAL_MODULE := acp
-LOCAL_CXX_STL := none
-
-include $(BUILD_HOST_EXECUTABLE)
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
new file mode 100644
index 0000000..931c7b2
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
@@ -0,0 +1,458 @@
+/*
+ * 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;
+
+import com.android.apksigner.core.apk.ApkUtils;
+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.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * APK signature verifier which mimics the behavior of the Android platform.
+ *
+ * <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
+ * the verifier to be used for checking whether an APK's signatures will verify on Android.
+ */
+public class ApkVerifier {
+
+    /**
+     * 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}.
+     * The verification result also includes errors, warnings, and information about signers.
+     *
+     * @param apk APK file contents
+     * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures
+     *        may need to be verified
+     *
+     * @throws IOException if an I/O error is encountered while reading the APK
+     * @throws ZipFormatException if the APK is malformed at ZIP format level
+     */
+    public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException {
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        // Attempt to verify the APK using APK Signature Scheme v2
+        Result result = new Result();
+        try {
+            V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
+            result.mergeFrom(v2Result);
+        } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // TODO: Verify JAR signature if necessary
+        if (!result.isVerifiedUsingV2Scheme()) {
+            return result;
+        }
+
+        // Verified
+        result.setVerified();
+        for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+            result.addSignerCertificate(signerInfo.getCertificate());
+        }
+
+        return result;
+    }
+
+    /**
+     * Result of verifying an APKs signatures. The APK can be considered verified iff
+     * {@link #isVerified()} returns {@code true}.
+     */
+    public static class Result {
+        private final List<IssueWithParams> mErrors = new ArrayList<>();
+        private final List<IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<X509Certificate> mSignerCerts = new ArrayList<>();
+        private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
+
+        private boolean mVerified;
+        private boolean mVerifiedUsingV2Scheme;
+
+        /**
+         * Returns {@code true} if the APK's signatures verified.
+         */
+        public boolean isVerified() {
+            return mVerified;
+        }
+
+        private void setVerified() {
+            mVerified = true;
+        }
+
+        /**
+         * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
+         */
+        public boolean isVerifiedUsingV2Scheme() {
+            return mVerifiedUsingV2Scheme;
+        }
+
+        /**
+         * Returns the verified signers' certificates, one per signer.
+         */
+        public List<X509Certificate> getSignerCertificates() {
+            return mSignerCerts;
+        }
+
+        private void addSignerCertificate(X509Certificate cert) {
+            mSignerCerts.add(cert);
+        }
+
+        /**
+         * Returns information about APK Signature Scheme v2 signers associated with the APK's
+         * signature.
+         */
+        public List<V2SchemeSignerInfo> getV2SchemeSigners() {
+            return mV2SchemeSigners;
+        }
+
+        /**
+         * Returns errors encountered while verifying the APK's signatures.
+         */
+        public List<IssueWithParams> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * Returns warnings encountered while verifying the APK's signatures.
+         */
+        public List<IssueWithParams> getWarnings() {
+            return mWarnings;
+        }
+
+        private void mergeFrom(V2SchemeVerifier.Result source) {
+            mVerifiedUsingV2Scheme = source.verified;
+            mErrors.addAll(source.getErrors());
+            mWarnings.addAll(source.getWarnings());
+            for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) {
+                mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+            }
+        }
+
+        /**
+         * Returns {@code true} if an error was encountered while verifying the APK. Any error
+         * prevents the APK from being considered verified.
+         */
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            if (!mV2SchemeSigners.isEmpty()) {
+                for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
+         */
+        public static class V2SchemeSignerInfo {
+            private final int mIndex;
+            private final List<X509Certificate> mCerts;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+
+            private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) {
+                mIndex = result.index;
+                mCerts = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+            }
+
+            /**
+             * Returns this signer's {@code 0}-based index in the list of signers contained in the
+             * APK's APK Signature Scheme v2 signature.
+             */
+            public int getIndex() {
+                return mIndex;
+            }
+
+            /**
+             * 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 mCerts.isEmpty() ? null : mCerts.get(0);
+            }
+
+            /**
+             * Returns this signer's certificates. The first certificate is for the signer's public
+             * key. An empty list may be returned if an error was encountered during verification
+             * (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificates() {
+                return mCerts;
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+        }
+    }
+
+    /**
+     * Error or warning encountered while verifying an APK's signatures.
+     */
+    public static enum Issue {
+
+        /**
+         * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
+         */
+        V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+        /**
+         * Failed to parse this signer's signer block contained in the APK Signature Scheme v2
+         * signature.
+         */
+        V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+        /**
+         * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
+         * parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+        /**
+         * This APK Signature Scheme v2 signer's certificate could not be parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+         *     certificates ({@code Integer})</li>
+         * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+         *     list of certificates ({@code Integer})</li>
+         * <li>Parameter 3: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+        /**
+         * Failed to parse this signer's signature record contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
+
+        /**
+         * Failed to parse this signer's digest record contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+        /**
+         * APK Signature Scheme v2 signature contains no signers.
+         */
+        V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains a signature produced using an unknown
+         * algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+        /**
+         * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute ID ({@code Integer})</li>
+         * </ul>
+         */
+        V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+        /**
+         * An exception was encountered while verifying APK Signature Scheme v2 signature of this
+         * signer.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * </ul>
+         */
+        V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers no signatures.
+         */
+        V2_SIG_NO_SIGNATURES("No signatures"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
+         */
+        V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+        /**
+         * This APK Signature Scheme v2 signer offers no certificates.
+         */
+        V2_SIG_NO_CERTIFICATES("No certificates"),
+
+        /**
+         * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
+         * not match the public key listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+         * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+         * </ul>
+         */
+        V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+                "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+        /**
+         * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
+         * record do not match the signature algorithms listed in the signatures record.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+         * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+         * </ul>
+         */
+        V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+                "Signature algorithms mismatch between signatures and digests records"
+                        + ": %1$s vs %2$s"),
+
+        /**
+         * The APK's digest does not match the digest contained in the APK Signature Scheme v2
+         * signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+         * </ul>
+         */
+        V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
+                "APK integrity check failed. %1$s digest mismatch."
+                        + " Expected: <%2$s>, actual: <%3$s>"),
+
+        /**
+         * APK Signing Block contains an unknown entry.
+         *
+         * <ul>
+         * <li>Parameter 1: entry ID ({@code Integer})</li>
+         * </ul>
+         */
+        APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
+
+        private final String mFormat;
+
+        private Issue(String format) {
+            mFormat = format;
+        }
+
+        /**
+         * Returns the format string suitable for combining the parameters of this issue into a
+         * readable string. See {@link java.util.Formatter} for format.
+         */
+        private String getFormat() {
+            return mFormat;
+        }
+    }
+
+    /**
+     * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
+     * form.
+     */
+    public static class IssueWithParams {
+        private final Issue mIssue;
+        private final Object[] mParams;
+
+        /**
+         * Constructs a new {@code IssueWithParams} of the specified type and with provided
+         * parameters.
+         */
+        public IssueWithParams(Issue issue, Object[] params) {
+            mIssue = issue;
+            mParams = params;
+        }
+
+        /**
+         * Returns the type of this issue.
+         */
+        public Issue getIssue() {
+            return mIssue;
+        }
+
+        /**
+         * Returns the parameters of this issue.
+         */
+        public Object[] getParams() {
+            return mParams.clone();
+        }
+
+        /**
+         * Returns a readable form of this issue.
+         */
+        @Override
+        public String toString() {
+            return String.format(mIssue.getFormat(), mParams);
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
index dae3c5e..612f4fd 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
@@ -18,9 +18,9 @@
 
 import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
 import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
-import com.android.apksigner.core.internal.apk.v2.MessageDigestSink;
 import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
 import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink;
+import com.android.apksigner.core.internal.util.MessageDigestSink;
 import com.android.apksigner.core.internal.util.Pair;
 import com.android.apksigner.core.util.DataSink;
 import com.android.apksigner.core.util.DataSource;
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java
new file mode 100644
index 0000000..8cc8c90
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java
@@ -0,0 +1,158 @@
+/*
+ * 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.apk;
+
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * APK utilities.
+ */
+public class ApkUtils {
+
+    private ApkUtils() {}
+
+    /**
+     * Finds the main ZIP sections of the provided APK.
+     *
+     * @throws IOException if an I/O error occurred while reading the APK
+     * @throws ZipFormatException if the APK is malformed
+     */
+    public static ZipSections findZipSections(DataSource apk)
+            throws IOException, ZipFormatException {
+        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+        if (eocdAndOffsetInFile == null) {
+            throw new ZipFormatException("ZIP End of Central Directory record not found");
+        }
+
+        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+        long eocdOffset = eocdAndOffsetInFile.getSecond();
+        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
+            throw new ZipFormatException("ZIP64 APK not supported");
+        }
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
+        if (cdStartOffset >= eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory start offset out of range: " + cdStartOffset
+                        + ". ZIP End of Central Directory offset: " + eocdOffset);
+        }
+
+        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
+        long cdEndOffset = cdStartOffset + cdSizeBytes;
+        if (cdEndOffset > eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory overlaps with End of Central Directory"
+                            + ". CD end: " + cdEndOffset
+                            + ", EoCD start: " + eocdOffset);
+        }
+
+        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
+
+        return new ZipSections(
+                cdStartOffset,
+                cdSizeBytes,
+                cdRecordCount,
+                eocdOffset,
+                eocdBuf);
+    }
+
+    /**
+     * Information about the ZIP sections of an APK.
+     */
+    public static class ZipSections {
+        private final long mCentralDirectoryOffset;
+        private final long mCentralDirectorySizeBytes;
+        private final int mCentralDirectoryRecordCount;
+        private final long mEocdOffset;
+        private final ByteBuffer mEocd;
+
+        public ZipSections(
+                long centralDirectoryOffset,
+                long centralDirectorySizeBytes,
+                int centralDirectoryRecordCount,
+                long eocdOffset,
+                ByteBuffer eocd) {
+            mCentralDirectoryOffset = centralDirectoryOffset;
+            mCentralDirectorySizeBytes = centralDirectorySizeBytes;
+            mCentralDirectoryRecordCount = centralDirectoryRecordCount;
+            mEocdOffset = eocdOffset;
+            mEocd = eocd;
+        }
+
+        /**
+         * Returns the start offset of the ZIP Central Directory. This value is taken from the
+         * ZIP End of Central Directory record.
+         */
+        public long getZipCentralDirectoryOffset() {
+            return mCentralDirectoryOffset;
+        }
+
+        /**
+         * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
+         * ZIP End of Central Directory record.
+         */
+        public long getZipCentralDirectorySizeBytes() {
+            return mCentralDirectorySizeBytes;
+        }
+
+        /**
+         * Returns the number of records in the ZIP Central Directory. This value is taken from the
+         * ZIP End of Central Directory record.
+         */
+        public int getZipCentralDirectoryRecordCount() {
+            return mCentralDirectoryRecordCount;
+        }
+
+        /**
+         * Returns the start offset of the ZIP End of Central Directory record. The record extends
+         * until the very end of the APK.
+         */
+        public long getZipEndOfCentralDirectoryOffset() {
+            return mEocdOffset;
+        }
+
+        /**
+         * Returns the contents of the ZIP End of Central Directory.
+         */
+        public ByteBuffer getZipEndOfCentralDirectory() {
+            return mEocd;
+        }
+    }
+
+    /**
+     * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
+     * Directory record.
+     *
+     * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+     * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
+     *        be between {@code 0} and {@code 2^32 - 1} inclusive.
+     */
+    public static void setZipEocdCentralDirectoryOffset(
+            ByteBuffer zipEndOfCentralDirectory, long offset) {
+        ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+        eocd.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
+    }
+}
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 8b59b8e..9f4ccce 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
@@ -41,13 +41,20 @@
 import java.util.jar.Manifest;
 
 import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.DERNull;
 import org.bouncycastle.asn1.DEROutputStream;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
 import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
 import org.bouncycastle.cms.CMSException;
 import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignatureEncryptionAlgorithmFinder;
 import org.bouncycastle.cms.CMSSignedData;
 import org.bouncycastle.cms.CMSSignedDataGenerator;
-import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder;
+import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
 import org.bouncycastle.operator.ContentSigner;
 import org.bouncycastle.operator.OperatorCreationException;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
@@ -462,10 +469,11 @@
                     .build(signerConfig.privateKey);
             CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
             gen.addSignerInfoGenerator(
-                    new JcaSignerInfoGeneratorBuilder(
-                            new JcaDigestCalculatorProviderBuilder().build())
-                    .setDirectSignature(true)
-                    .build(signer, signerCert));
+                    new SignerInfoGeneratorBuilder(
+                            new JcaDigestCalculatorProviderBuilder().build(),
+                            SignerInfoSignatureAlgorithmFinder.INSTANCE)
+                            .setDirectSignature(true)
+                            .build(signer, new JcaX509CertificateHolder(signerCert)));
             gen.addCertificates(certs);
 
             CMSSignedData sigData =
@@ -482,6 +490,37 @@
         }
     }
 
+    /**
+     * Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo.
+     */
+    private static class SignerInfoSignatureAlgorithmFinder
+            implements CMSSignatureEncryptionAlgorithmFinder {
+        private static final SignerInfoSignatureAlgorithmFinder INSTANCE =
+                new SignerInfoSignatureAlgorithmFinder();
+
+        private static final AlgorithmIdentifier DSA =
+                new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, DERNull.INSTANCE);
+
+        private final CMSSignatureEncryptionAlgorithmFinder mDefault =
+                new DefaultCMSSignatureEncryptionAlgorithmFinder();
+
+        @Override
+        public AlgorithmIdentifier findEncryptionAlgorithm(AlgorithmIdentifier id) {
+            // Use the default chooser, but replace dsaWithSha1 with dsa. This is because "dsa" is
+            // accepted by any Android platform whereas "dsaWithSha1" is accepted only since
+            // API Level 9.
+            id = mDefault.findEncryptionAlgorithm(id);
+            if (id != null) {
+                ASN1ObjectIdentifier oid = id.getAlgorithm();
+                if (X9ObjectIdentifiers.id_dsa_with_sha1.equals(oid)) {
+                    return DSA;
+                }
+            }
+
+            return id;
+        }
+    }
+
     private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
         switch (digestAlgorithm) {
             case SHA1:
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
index cb0f84a..7c136be 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
@@ -19,7 +19,7 @@
 /**
  * APK Signature Scheme v2 content digest algorithm.
  */
-enum ContentDigestAlgorithm {
+public enum ContentDigestAlgorithm {
     /** SHA2-256 over 1 MB chunks. */
     CHUNKED_SHA256("SHA-256", 256 / 8),
 
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
index 3c7b5f0..20f890d 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
@@ -23,7 +23,7 @@
 import java.security.spec.PSSParameterSpec;
 
 /**
- * APK Signature Scheme v2 content digest algorithm.
+ * APK Signature Scheme v2 signature algorithm.
  */
 public enum SignatureAlgorithm {
     /**
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
index e185346..aba390b 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
@@ -16,7 +16,7 @@
 
 package com.android.apksigner.core.internal.apk.v2;
 
-import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.internal.util.MessageDigestSink;
 import com.android.apksigner.core.internal.util.Pair;
 import com.android.apksigner.core.internal.zip.ZipUtils;
 import com.android.apksigner.core.util.DataSource;
@@ -191,8 +191,10 @@
         // offset field is treated as pointing to the offset at which the APK Signing Block will
         // start.
         long centralDirOffsetForDigesting = beforeCentralDir.size();
-        ByteBuffer eocdBuf = copyToByteBuffer(eocd);
+        ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
         eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        eocd.copyTo(0, (int) eocd.size(), eocdBuf);
+        eocdBuf.flip();
         ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
 
         // Compute digests of APK contents.
@@ -215,7 +217,7 @@
         return generateApkSigningBlock(signerConfigs, contentDigests);
     }
 
-    private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+    static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
             Set<ContentDigestAlgorithm> digestAlgorithms,
             DataSource[] contents) throws IOException, DigestException {
         // For each digest algorithm the result is computed as follows:
@@ -600,15 +602,4 @@
         }
         return result.array();
     }
-
-    private static ByteBuffer copyToByteBuffer(DataSource dataSource) throws IOException {
-        long dataSourceSize = dataSource.size();
-        if (dataSourceSize > Integer.MAX_VALUE) {
-            throw new IllegalArgumentException("Data source too large: " + dataSourceSize);
-        }
-        ByteBuffer result = ByteBuffer.allocate((int) dataSourceSize);
-        dataSource.feed(0, result.remaining(), new ByteBufferSink(result));
-        result.position(0);
-        return result;
-    }
 }
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
new file mode 100644
index 0000000..efefb00
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
@@ -0,0 +1,939 @@
+/*
+ * 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.v2;
+
+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.util.ByteBufferDataSource;
+import com.android.apksigner.core.internal.util.DelegatingX509Certificate;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme v2 verifier.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
+ */
+public abstract class V2SchemeVerifier {
+
+    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+    private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SchemeVerifier() {}
+
+    /**
+     * Verifies the provided APK's APK Signature Scheme v2 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 SignatureNotFoundException if no APK Signature Scheme v2 signatures are found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws IOException, SignatureNotFoundException {
+        Result result = new Result();
+        SignatureInfo signatureInfo = findSignature(apk, zipSections, result);
+
+        DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
+        DataSource centralDir =
+                apk.slice(
+                        signatureInfo.centralDirOffset,
+                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+        ByteBuffer eocd = signatureInfo.eocd;
+
+        verify(beforeApkSigningBlock,
+                signatureInfo.signatureBlock,
+                centralDir,
+                eocd,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's v2 signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the
+     * {@code result}.
+     */
+    private static void verify(
+            DataSource beforeApkSigningBlock,
+            ByteBuffer apkSignatureSchemeV2Block,
+            DataSource centralDir,
+            ByteBuffer eocd,
+            Result result) throws IOException {
+        Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        parseSigners(apkSignatureSchemeV2Block, contentDigestsToVerify, result);
+        if (result.containsErrors()) {
+            return;
+        }
+        verifyIntegrity(
+                beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+        if (!result.containsErrors()) {
+            result.verified = true;
+        }
+    }
+
+    /**
+     * Parses each signer in the provided APK Signature Scheme v2 block and populates
+     * {@code signerInfos} of the provided {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+     * However, this does not verify the integrity of the rest of the APK but rather simply reports
+     * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
+     */
+    private static void parseSigners(
+            ByteBuffer apkSignatureSchemeV2Block,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            Result result) {
+        ByteBuffer signers;
+        try {
+            signers = getLengthPrefixedSlice(apkSignatureSchemeV2Block);
+        } catch (IOException e) {
+            result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
+            return;
+        }
+        if (!signers.hasRemaining()) {
+            result.addError(Issue.V2_SIG_NO_SIGNERS);
+            return;
+        }
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        }
+        int signerCount = 0;
+        while (signers.hasRemaining()) {
+            int signerIndex = signerCount;
+            signerCount++;
+            Result.SignerInfo signerInfo = new Result.SignerInfo();
+            signerInfo.index = signerIndex;
+            result.signers.add(signerInfo);
+            try {
+                ByteBuffer signer = getLengthPrefixedSlice(signers);
+                parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
+            } catch (IOException | BufferUnderflowException e) {
+                signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Parses the provided signer block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+     * verify the integrity of the rest of the APK. Rather, this method adds to the
+     * {@code contentDigestsToVerify}.
+     */
+    private static void parseSigner(
+            ByteBuffer signerBlock,
+            CertificateFactory certFactory,
+            Result.SignerInfo result,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify) throws IOException {
+        ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+        byte[] signedDataBytes = new byte[signedData.remaining()];
+        signedData.get(signedDataBytes);
+        signedData.flip();
+        result.signedData = signedDataBytes;
+
+        ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+        byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+        // Parse the signatures block and identify supported signatures
+        int signatureCount = 0;
+        List<SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = readLengthPrefixedByteArray(signature);
+                result.signatures.add(
+                        new Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                    continue;
+                }
+                supportedSignatures.add(new SupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (IOException | BufferUnderflowException e) {
+                result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (result.signatures.isEmpty()) {
+            result.addError(Issue.V2_SIG_NO_SIGNATURES);
+            return;
+        }
+
+        // Verify signatures over signed-data block using the public key
+        List<SupportedSignature> signaturesToVerify = getSignaturesToVerify(supportedSignatures);
+        if (signaturesToVerify.isEmpty()) {
+            result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
+            return;
+        }
+        for (SupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+            PublicKey publicKey;
+            try {
+                publicKey =
+                        KeyFactory.getInstance(keyAlgorithm).generatePublic(
+                                new X509EncodedKeySpec(publicKeyBytes));
+            } catch (Exception e) {
+                result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
+                return;
+            }
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                signedData.position(0);
+                sig.update(signedData);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+                result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+                contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+            } catch (Exception e) {
+                result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+
+        // At least one signature over signedData has verified. We can now parse signed-data.
+        signedData.position(0);
+        ByteBuffer digests = getLengthPrefixedSlice(signedData);
+        ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+        ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+
+        // Parse the certificates block
+        int certificateIndex = -1;
+        while (certificates.hasRemaining()) {
+            certificateIndex++;
+            byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+            X509Certificate certificate;
+            try {
+                certificate =
+                        (X509Certificate)
+                                certFactory.generateCertificate(
+                                        new ByteArrayInputStream(encodedCert));
+            } catch (CertificateException e) {
+                result.addError(
+                        Issue.V2_SIG_MALFORMED_CERTIFICATE,
+                        certificateIndex,
+                        certificateIndex + 1,
+                        e);
+                return;
+            }
+            // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+            // form. Without this, getEncoded may return a different form from what was stored in
+            // the signature. This is becase some X509Certificate(Factory) implementations re-encode
+            // certificates.
+            certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+            result.certs.add(certificate);
+        }
+
+        if (result.certs.isEmpty()) {
+            result.addError(Issue.V2_SIG_NO_CERTIFICATES);
+            return;
+        }
+        X509Certificate mainCertificate = result.certs.get(0);
+        byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+            result.addError(
+                    Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+                    toHex(certificatePublicKeyBytes),
+                    toHex(publicKeyBytes));
+            return;
+        }
+
+        // Parse the digests block
+        int digestCount = 0;
+        while (digests.hasRemaining()) {
+            digestCount++;
+            try {
+                ByteBuffer digest = getLengthPrefixedSlice(digests);
+                int sigAlgorithmId = digest.getInt();
+                byte[] digestBytes = readLengthPrefixedByteArray(digest);
+                result.contentDigests.add(
+                        new Result.SignerInfo.ContentDigest(sigAlgorithmId, digestBytes));
+            } catch (IOException | BufferUnderflowException e) {
+                result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
+                return;
+            }
+        }
+
+        List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+        for (Result.SignerInfo.Signature signature : result.signatures) {
+            sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+        }
+        List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+        for (Result.SignerInfo.ContentDigest digest : result.contentDigests) {
+            sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
+        }
+
+        if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
+            result.addError(
+                    Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
+                    sigAlgsFromSignaturesRecord,
+                    sigAlgsFromDigestsRecord);
+            return;
+        }
+
+        // Parse the additional attributes block.
+        int additionalAttributeCount = 0;
+        while (additionalAttributes.hasRemaining()) {
+            additionalAttributeCount++;
+            try {
+                ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+                int id = attribute.getInt();
+                byte[] value = readLengthPrefixedByteArray(attribute);
+                result.additionalAttributes.add(
+                        new Result.SignerInfo.AdditionalAttribute(id, value));
+                result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+            } catch (IOException | BufferUnderflowException e) {
+                result.addError(
+                        Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+                return;
+            }
+        }
+    }
+
+    private static List<SupportedSignature> getSignaturesToVerify(
+            List<SupportedSignature> signatures) {
+        // Pick the signature with the strongest algorithm, to mimic Android's behavior.
+        SignatureAlgorithm bestSigAlgorithm = null;
+        byte[] bestSigAlgorithmSignatureBytes = null;
+        for (SupportedSignature sig : signatures) {
+            SignatureAlgorithm sigAlgorithm = sig.algorithm;
+            if ((bestSigAlgorithm == null)
+                    || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+                bestSigAlgorithm = sigAlgorithm;
+                bestSigAlgorithmSignatureBytes = sig.signature;
+            }
+        }
+
+        if (bestSigAlgorithm == null) {
+            return Collections.emptyList();
+        } else {
+            return Collections.singletonList(
+                    new SupportedSignature(bestSigAlgorithm, bestSigAlgorithmSignatureBytes));
+        }
+    }
+
+    private static class SupportedSignature {
+        private final SignatureAlgorithm algorithm;
+        private final byte[] signature;
+
+        private SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+            this.algorithm = algorithm;
+            this.signature = signature;
+        }
+    }
+
+    /**
+     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+     */
+    private static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+        ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+        ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+        return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+    }
+
+    /**
+     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+     */
+    private static int compareContentDigestAlgorithm(
+            ContentDigestAlgorithm alg1,
+            ContentDigestAlgorithm alg2) {
+        switch (alg1) {
+            case CHUNKED_SHA256:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 0;
+                    case CHUNKED_SHA512:
+                        return -1;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            case CHUNKED_SHA512:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 1;
+                    case CHUNKED_SHA512:
+                        return 0;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            default:
+                throw new IllegalArgumentException("Unknown alg1: " + alg1);
+        }
+    }
+
+    /**
+     * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
+     * APK and comparing them against the digests listed in APK Signing Block. The expected digests
+     * taken from {@code v2SchemeSignerInfos} of the provided {@code result}.
+     */
+    private static void verifyIntegrity(
+            DataSource beforeApkSigningBlock,
+            DataSource centralDir,
+            ByteBuffer eocd,
+            Set<ContentDigestAlgorithm> contentDigestAlgorithms,
+            Result result) throws IOException {
+        if (contentDigestAlgorithms.isEmpty()) {
+            // This should never occur because this method is invoked once at least one signature
+            // is verified, meaning at least one content digest is known.
+            throw new RuntimeException("No content digests found");
+        }
+
+        // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
+        // treated as though its Central Directory offset points to the start of APK Signing Block.
+        // We thus modify the EoCD accordingly.
+        ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
+        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+        modifiedEocd.put(eocd);
+        modifiedEocd.flip();
+        ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
+        Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
+        try {
+            actualContentDigests =
+                    V2SchemeSigner.computeContentDigests(
+                            contentDigestAlgorithms,
+                            new DataSource[] {
+                                    beforeApkSigningBlock,
+                                    centralDir,
+                                    new ByteBufferDataSource(modifiedEocd)
+                            });
+        } catch (DigestException e) {
+            throw new RuntimeException("Failed to compute content digests", e);
+        }
+        if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
+            throw new RuntimeException(
+                    "Mismatch between sets of requested and computed content digests"
+                            + " . Requested: " + contentDigestAlgorithms
+                            + ", computed: " + actualContentDigests.keySet());
+        }
+
+        // Compare digests computed over the rest of APK against the corresponding expected digests
+        // in signer blocks.
+        for (Result.SignerInfo signerInfo : result.signers) {
+            for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
+                SignatureAlgorithm signatureAlgorithm =
+                        SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                ContentDigestAlgorithm contentDigestAlgorithm =
+                        signatureAlgorithm.getContentDigestAlgorithm();
+                byte[] expectedDigest = expected.getValue();
+                byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
+                if (!Arrays.equals(expectedDigest, actualDigest)) {
+                    signerInfo.addError(
+                            Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
+                            contentDigestAlgorithm,
+                            toHex(expectedDigest),
+                            toHex(actualDigest));
+                    continue;
+                }
+                signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+            }
+        }
+    }
+
+    /**
+     * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
+     * contained in the block against the file.
+     */
+    private static class SignatureInfo {
+        /** Contents of APK Signature Scheme v2 block. */
+        private final ByteBuffer signatureBlock;
+
+        /** Position of the APK Signing Block in the file. */
+        private final long apkSigningBlockOffset;
+
+        /** Position of the ZIP Central Directory in the file. */
+        private final long centralDirOffset;
+
+        /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+        private final long eocdOffset;
+
+        /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+        private final ByteBuffer eocd;
+
+        private SignatureInfo(
+                ByteBuffer signatureBlock,
+                long apkSigningBlockOffset,
+                long centralDirOffset,
+                long eocdOffset,
+                ByteBuffer eocd) {
+            this.signatureBlock = signatureBlock;
+            this.apkSigningBlockOffset = apkSigningBlockOffset;
+            this.centralDirOffset = centralDirOffset;
+            this.eocdOffset = eocdOffset;
+            this.eocd = eocd;
+        }
+    }
+
+    /**
+     * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
+     * additional information relevant for verifying the block against the file.
+     *
+     * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2
+     * @throws IOException if an I/O error occurs while reading the APK
+     */
+    private static SignatureInfo findSignature(
+            DataSource apk, ApkUtils.ZipSections zipSections, Result result)
+                    throws IOException, SignatureNotFoundException {
+        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+        long centralDirEndOffset =
+                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+        if (centralDirEndOffset != eocdStartOffset) {
+            throw new SignatureNotFoundException(
+                    "ZIP Central Directory is not immediately followed by End of Central Directory"
+                            + ". CD end: " + centralDirEndOffset
+                            + ", EoCD start: " + eocdStartOffset);
+        }
+
+        // Find the APK Signing Block. The block immediately precedes the Central Directory.
+        ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
+        Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
+                findApkSigningBlock(apk, centralDirStartOffset);
+        ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
+        long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+
+        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
+        ByteBuffer apkSignatureSchemeV2Block =
+                findApkSignatureSchemeV2Block(apkSigningBlock, result);
+
+        return new SignatureInfo(
+                apkSignatureSchemeV2Block,
+                apkSigningBlockOffset,
+                centralDirStartOffset,
+                eocdStartOffset,
+                eocd);
+    }
+
+    private static Pair<ByteBuffer, Long> findApkSigningBlock(
+            DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
+        // FORMAT:
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes payload
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+
+        if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+            throw new SignatureNotFoundException(
+                    "APK too small for APK Signing Block. ZIP Central Directory offset: "
+                            + centralDirOffset);
+        }
+        // Read the magic and offset in file from the footer section of the block:
+        // * uint64:   size of block
+        // * 16 bytes: magic
+        ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24);
+        footer.order(ByteOrder.LITTLE_ENDIAN);
+        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+            throw new SignatureNotFoundException(
+                    "No APK Signing Block before ZIP Central Directory");
+        }
+        // Read and compare size fields
+        long apkSigBlockSizeInFooter = footer.getLong(0);
+        if ((apkSigBlockSizeInFooter < footer.capacity())
+                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+            throw new SignatureNotFoundException(
+                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+        }
+        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+        long apkSigBlockOffset = centralDirOffset - totalSize;
+        if (apkSigBlockOffset < 0) {
+            throw new SignatureNotFoundException(
+                    "APK Signing Block offset out of range: " + apkSigBlockOffset);
+        }
+        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize);
+        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+            throw new SignatureNotFoundException(
+                    "APK Signing Block sizes in header and footer do not match: "
+                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+        }
+        return Pair.of(apkSigBlock, apkSigBlockOffset);
+    }
+
+    private static ByteBuffer findApkSignatureSchemeV2Block(
+            ByteBuffer apkSigningBlock,
+            Result result) throws SignatureNotFoundException {
+        checkByteOrderLittleEndian(apkSigningBlock);
+        // FORMAT:
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes pairs
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+        int entryCount = 0;
+        while (pairs.hasRemaining()) {
+            entryCount++;
+            if (pairs.remaining() < 8) {
+                throw new SignatureNotFoundException(
+                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+            }
+            long lenLong = pairs.getLong();
+            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount
+                                + " size out of range: " + lenLong);
+            }
+            int len = (int) lenLong;
+            int nextEntryPos = pairs.position() + len;
+            if (len > pairs.remaining()) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
+                                + ", available: " + pairs.remaining());
+            }
+            int id = pairs.getInt();
+            if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+                return getByteBuffer(pairs, len - 4);
+            }
+            result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
+            pairs.position(nextEntryPos);
+        }
+
+        throw new SignatureNotFoundException(
+                "No APK Signature Scheme v2 block in APK Signing Block");
+    }
+
+    private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+        }
+    }
+
+    public static class SignatureNotFoundException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public SignatureNotFoundException(String message) {
+            super(message);
+        }
+
+        public SignatureNotFoundException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+     * buffer's byte order.
+     */
+    private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+        if (start < 0) {
+            throw new IllegalArgumentException("start: " + start);
+        }
+        if (end < start) {
+            throw new IllegalArgumentException("end < start: " + end + " < " + start);
+        }
+        int capacity = source.capacity();
+        if (end > source.capacity()) {
+            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+        }
+        int originalLimit = source.limit();
+        int originalPosition = source.position();
+        try {
+            source.position(0);
+            source.limit(end);
+            source.position(start);
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            return result;
+        } finally {
+            source.position(0);
+            source.limit(originalLimit);
+            source.position(originalPosition);
+        }
+    }
+
+    /**
+     * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+     * position of this buffer.
+     *
+     * <p>This method reads the next {@code size} bytes at this buffer's current position,
+     * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+     * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+     * {@code size}.
+     */
+    private static ByteBuffer getByteBuffer(ByteBuffer source, int size)
+            throws BufferUnderflowException {
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        int originalLimit = source.limit();
+        int position = source.position();
+        int limit = position + size;
+        if ((limit < position) || (limit > originalLimit)) {
+            throw new BufferUnderflowException();
+        }
+        source.limit(limit);
+        try {
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            source.position(limit);
+            return result;
+        } finally {
+            source.limit(originalLimit);
+        }
+    }
+
+    private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
+        if (source.remaining() < 4) {
+            throw new IOException(
+                    "Remaining buffer too short to contain length of length-prefixed field."
+                            + " Remaining: " + source.remaining());
+        }
+        int len = source.getInt();
+        if (len < 0) {
+            throw new IllegalArgumentException("Negative length");
+        } else if (len > source.remaining()) {
+            throw new IOException("Length-prefixed field longer than remaining buffer."
+                    + " Field length: " + len + ", remaining: " + source.remaining());
+        }
+        return getByteBuffer(source, len);
+    }
+
+    private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
+        int len = buf.getInt();
+        if (len < 0) {
+            throw new IOException("Negative length");
+        } else if (len > buf.remaining()) {
+            throw new IOException("Underflow while reading length-prefixed value. Length: " + len
+                    + ", available: " + buf.remaining());
+        }
+        byte[] result = new byte[len];
+        buf.get(result);
+        return result;
+    }
+
+    /**
+     * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
+     * time.
+     */
+    private static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
+        private byte[] mEncodedForm;
+
+        public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
+            super(wrapped);
+            this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
+        }
+
+        @Override
+        public byte[] getEncoded() throws CertificateEncodingException {
+            return (mEncodedForm != null) ? mEncodedForm.clone() : null;
+        }
+    }
+
+    private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
+
+    private static String toHex(byte[] value) {
+        StringBuilder sb = new StringBuilder(value.length * 2);
+        int len = value.length;
+        for (int i = 0; i < len; i++) {
+            int hi = (value[i] & 0xff) >>> 4;
+            int lo = value[i] & 0x0f;
+            sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+        }
+        return sb.toString();
+    }
+
+    public static class Result {
+
+        /** Whether the APK's APK Signature Scheme v2 signature verifies. */
+        public boolean verified;
+
+        public final List<SignerInfo> signers = new ArrayList<>();
+        private final List<IssueWithParams> mWarnings = new ArrayList<>();
+        private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            if (!signers.isEmpty()) {
+                for (SignerInfo signer : signers) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        public void addError(Issue msg, Object... parameters) {
+            mErrors.add(new IssueWithParams(msg, parameters));
+        }
+
+        public 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 int index;
+            public List<X509Certificate> certs = new ArrayList<>();
+            public List<ContentDigest> contentDigests = new ArrayList<>();
+            public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
+            public List<Signature> signatures = new ArrayList<>();
+            public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
+            public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
+            public byte[] signedData;
+
+            private final List<IssueWithParams> mWarnings = new ArrayList<>();
+            private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+            public void addError(Issue msg, Object... parameters) {
+                mErrors.add(new IssueWithParams(msg, parameters));
+            }
+
+            public void addWarning(Issue msg, Object... parameters) {
+                mWarnings.add(new IssueWithParams(msg, parameters));
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public static class ContentDigest {
+                private final int mSignatureAlgorithmId;
+                private final byte[] mValue;
+
+                public ContentDigest(int signatureAlgorithmId, byte[] value) {
+                    mSignatureAlgorithmId  = signatureAlgorithmId;
+                    mValue = value;
+                }
+
+                public int getSignatureAlgorithmId() {
+                    return mSignatureAlgorithmId;
+                }
+
+                public byte[] getValue() {
+                    return mValue;
+                }
+            }
+
+            public static class Signature {
+                private final int mAlgorithmId;
+                private final byte[] mValue;
+
+                public Signature(int algorithmId, byte[] value) {
+                    mAlgorithmId  = algorithmId;
+                    mValue = value;
+                }
+
+                public int getAlgorithmId() {
+                    return mAlgorithmId;
+                }
+
+                public byte[] getValue() {
+                    return mValue;
+                }
+            }
+
+            public static class AdditionalAttribute {
+                private final int mId;
+                private final byte[] mValue;
+
+                public AdditionalAttribute(int id, byte[] value) {
+                    mId  = id;
+                    mValue = value.clone();
+                }
+
+                public int getId() {
+                    return mId;
+                }
+
+                public byte[] getValue() {
+                    return mValue.clone();
+                }
+            }
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
index f12f02e..b2d9ca1 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
@@ -35,7 +35,15 @@
      * buffer between the buffer's position and limit.
      */
     public ByteBufferDataSource(ByteBuffer buffer) {
-        mBuffer = buffer.slice();
+        this(buffer, true);
+    }
+
+    /**
+     * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+     * buffer between the buffer's position and limit.
+     */
+    private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
+        mBuffer = (sliceRequired) ? buffer.slice() : buffer;
         mSize = buffer.remaining();
     }
 
@@ -45,15 +53,12 @@
     }
 
     @Override
-    public ByteBufferDataSource slice(long offset, long size) {
-        if ((offset == 0) && (size == mSize)) {
-            return this;
-        }
+    public ByteBuffer getByteBuffer(long offset, int size) {
         checkChunkValid(offset, size);
 
-        // checkChunkValid ensures that it's OK to cast offset and size to int.
+        // checkChunkValid ensures that it's OK to cast offset to int.
         int chunkPosition = (int) offset;
-        int chunkLimit = (int) (chunkPosition + size);
+        int chunkLimit = chunkPosition + size;
         // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
         // and limit fields, to be more specific). We thus use synchronization around these
         // state-changing operations to make instances of this class thread-safe.
@@ -66,35 +71,35 @@
 
             mBuffer.limit(chunkLimit);
             mBuffer.position(chunkPosition);
-            // Create a ByteBufferDataSource for the slice of the buffer between limit and position.
-            return new ByteBufferDataSource(mBuffer);
+            return mBuffer.slice();
         }
     }
 
     @Override
+    public void copyTo(long offset, int size, ByteBuffer dest) {
+        dest.put(getByteBuffer(offset, size));
+    }
+
+    @Override
     public void feed(long offset, long size, DataSink sink) throws IOException {
-        checkChunkValid(offset, size);
-
-        // checkChunkValid ensures that it's OK to cast offset and size to int.
-        int chunkPosition = (int) offset;
-        int chunkLimit = (int) (chunkPosition + size);
-        ByteBuffer chunk;
-        // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
-        // and limit fields, to be more specific). We thus use synchronization around these
-        // state-changing operations to make instances of this class thread-safe.
-        synchronized (mBuffer) {
-            // ByteBuffer.limit(int) and .position(int) check that that the position >= limit
-            // invariant is not broken. Thus, the only way to safely change position and limit
-            // without caring about their current values is to first set position to 0 or set the
-            // limit to capacity.
-            mBuffer.position(0);
-
-            mBuffer.limit(chunkLimit);
-            mBuffer.position(chunkPosition);
-            chunk = mBuffer.slice();
+        if ((size < 0) || (size > mSize)) {
+            throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
         }
+        sink.consume(getByteBuffer(offset, (int) size));
+    }
 
-        sink.consume(chunk);
+    @Override
+    public ByteBufferDataSource slice(long offset, long size) {
+        if ((offset == 0) && (size == mSize)) {
+            return this;
+        }
+        if ((size < 0) || (size > mSize)) {
+            throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
+        }
+        return new ByteBufferDataSource(
+                getByteBuffer(offset, (int) size),
+                false // no need to slice -- it's already a slice
+                );
     }
 
     private void checkChunkValid(long offset, long size) {
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java
new file mode 100644
index 0000000..936cfa9
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * {@link X509Certificate} which delegates all method invocations to the provided delegate
+ * {@code X509Certificate}.
+ */
+public class DelegatingX509Certificate extends X509Certificate {
+    private final X509Certificate mDelegate;
+
+    public DelegatingX509Certificate(X509Certificate delegate) {
+        this.mDelegate = delegate;
+    }
+
+    @Override
+    public Set<String> getCriticalExtensionOIDs() {
+        return mDelegate.getCriticalExtensionOIDs();
+    }
+
+    @Override
+    public byte[] getExtensionValue(String oid) {
+        return mDelegate.getExtensionValue(oid);
+    }
+
+    @Override
+    public Set<String> getNonCriticalExtensionOIDs() {
+        return mDelegate.getNonCriticalExtensionOIDs();
+    }
+
+    @Override
+    public boolean hasUnsupportedCriticalExtension() {
+        return mDelegate.hasUnsupportedCriticalExtension();
+    }
+
+    @Override
+    public void checkValidity()
+            throws CertificateExpiredException, CertificateNotYetValidException {
+        mDelegate.checkValidity();
+    }
+
+    @Override
+    public void checkValidity(Date date)
+            throws CertificateExpiredException, CertificateNotYetValidException {
+        mDelegate.checkValidity(date);
+    }
+
+    @Override
+    public int getVersion() {
+        return mDelegate.getVersion();
+    }
+
+    @Override
+    public BigInteger getSerialNumber() {
+        return mDelegate.getSerialNumber();
+    }
+
+    @Override
+    public Principal getIssuerDN() {
+        return mDelegate.getIssuerDN();
+    }
+
+    @Override
+    public Principal getSubjectDN() {
+        return mDelegate.getSubjectDN();
+    }
+
+    @Override
+    public Date getNotBefore() {
+        return mDelegate.getNotBefore();
+    }
+
+    @Override
+    public Date getNotAfter() {
+        return mDelegate.getNotAfter();
+    }
+
+    @Override
+    public byte[] getTBSCertificate() throws CertificateEncodingException {
+        return mDelegate.getTBSCertificate();
+    }
+
+    @Override
+    public byte[] getSignature() {
+        return mDelegate.getSignature();
+    }
+
+    @Override
+    public String getSigAlgName() {
+        return mDelegate.getSigAlgName();
+    }
+
+    @Override
+    public String getSigAlgOID() {
+        return mDelegate.getSigAlgOID();
+    }
+
+    @Override
+    public byte[] getSigAlgParams() {
+        return mDelegate.getSigAlgParams();
+    }
+
+    @Override
+    public boolean[] getIssuerUniqueID() {
+        return mDelegate.getIssuerUniqueID();
+    }
+
+    @Override
+    public boolean[] getSubjectUniqueID() {
+        return mDelegate.getSubjectUniqueID();
+    }
+
+    @Override
+    public boolean[] getKeyUsage() {
+        return mDelegate.getKeyUsage();
+    }
+
+    @Override
+    public int getBasicConstraints() {
+        return mDelegate.getBasicConstraints();
+    }
+
+    @Override
+    public byte[] getEncoded() throws CertificateEncodingException {
+        return mDelegate.getEncoded();
+    }
+
+    @Override
+    public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
+            InvalidKeyException, NoSuchProviderException, SignatureException {
+        mDelegate.verify(key);
+    }
+
+    @Override
+    public void verify(PublicKey key, String sigProvider)
+            throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
+            NoSuchProviderException, SignatureException {
+        mDelegate.verify(key, sigProvider);
+    }
+
+    @Override
+    public String toString() {
+        return mDelegate.toString();
+    }
+
+    @Override
+    public PublicKey getPublicKey() {
+        return mDelegate.getPublicKey();
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
similarity index 96%
rename from tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
rename to tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
index 9ef04bf..45bb30e 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.apksigner.core.internal.apk.v2;
+package com.android.apksigner.core.internal.util;
 
 import com.android.apksigner.core.util.DataSink;
 
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 7b47e50..5e724a2 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
@@ -16,9 +16,13 @@
 
 package com.android.apksigner.core.internal.zip;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.util.DataSource;
+
 /**
  * Assorted ZIP format helpers.
  *
@@ -28,7 +32,20 @@
 public abstract class ZipUtils {
     private ZipUtils() {}
 
+    public static final short COMPRESSION_METHOD_STORED = 0;
+    public static final short COMPRESSION_METHOD_DEFLATED = 8;
+
+    private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
+    private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
+    private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
+    private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
     private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+    private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
+
+    private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+    private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
+
+    private static final int UINT16_MAX_VALUE = 0xffff;
 
     /**
      * Sets the offset of the start of the ZIP Central Directory in the archive.
@@ -44,16 +61,218 @@
                 offset);
     }
 
+    /**
+     * Returns the offset of the start of the ZIP Central Directory in the archive.
+     *
+     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+     */
+    public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
+        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+        return getUnsignedInt32(
+                zipEndOfCentralDirectory,
+                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
+    }
+
+    /**
+     * Returns the size (in bytes) of the ZIP Central Directory.
+     *
+     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+     */
+    public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
+        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+        return getUnsignedInt32(
+                zipEndOfCentralDirectory,
+                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
+    }
+
+    /**
+     * Returns the total number of records in ZIP Central Directory.
+     *
+     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+     */
+    public static int getZipEocdCentralDirectoryTotalRecordCount(
+            ByteBuffer zipEndOfCentralDirectory) {
+        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+        return getUnsignedInt16(
+                zipEndOfCentralDirectory,
+                zipEndOfCentralDirectory.position()
+                        + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
+    }
+
+    /**
+     * Returns the ZIP End of Central Directory record of the provided ZIP file.
+     *
+     * @return contents of the ZIP End of Central Directory record and the record's offset in the
+     *         file or {@code null} if the file does not contain the record.
+     *
+     * @throws IOException if an I/O error occurs while reading the file.
+     */
+    public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
+            throws IOException {
+        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+        // The record can be identified by its 4-byte signature/magic which is located at the very
+        // beginning of the record. A complication is that the record is variable-length because of
+        // the comment field.
+        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+        // the candidate record's comment length is such that the remainder of the record takes up
+        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+        long fileSize = zip.size();
+        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+            return null;
+        }
+
+        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
+        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
+        // reading more data.
+        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
+        if (result != null) {
+            return result;
+        }
+
+        // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
+        // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
+        // the comment length field is an unsigned 16-bit number.
+        return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
+    }
+
+    /**
+     * Returns the ZIP End of Central Directory record of the provided ZIP file.
+     *
+     * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
+     *        value is from 0 to 65535 inclusive. The smaller the value, the faster this method
+     *        locates the record, provided its comment field is no longer than this value.
+     *
+     * @return contents of the ZIP End of Central Directory record and the record's offset in the
+     *         file or {@code null} if the file does not contain the record.
+     *
+     * @throws IOException if an I/O error occurs while reading the file.
+     */
+    private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
+            DataSource zip, int maxCommentSize) throws IOException {
+        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+        // The record can be identified by its 4-byte signature/magic which is located at the very
+        // beginning of the record. A complication is that the record is variable-length because of
+        // the comment field.
+        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+        // the candidate record's comment length is such that the remainder of the record takes up
+        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+        if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
+            throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
+        }
+
+        long fileSize = zip.size();
+        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+            // No space for EoCD record in the file.
+            return null;
+        }
+        // Lower maxCommentSize if the file is too small.
+        maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
+
+        int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
+        long bufOffsetInFile = fileSize - maxEocdSize;
+        ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
+        buf.order(ByteOrder.LITTLE_ENDIAN);
+        int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
+        if (eocdOffsetInBuf == -1) {
+            // No EoCD record found in the buffer
+            return null;
+        }
+        // EoCD found
+        buf.position(eocdOffsetInBuf);
+        ByteBuffer eocd = buf.slice();
+        eocd.order(ByteOrder.LITTLE_ENDIAN);
+        return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
+    }
+
+    /**
+     * Returns the position at which ZIP End of Central Directory record starts in the provided
+     * buffer or {@code -1} if the record is not present.
+     *
+     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
+     */
+    private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
+        assertByteOrderLittleEndian(zipContents);
+
+        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+        // The record can be identified by its 4-byte signature/magic which is located at the very
+        // beginning of the record. A complication is that the record is variable-length because of
+        // the comment field.
+        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+        // the candidate record's comment length is such that the remainder of the record takes up
+        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+        int archiveSize = zipContents.capacity();
+        if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
+            return -1;
+        }
+        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
+        int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
+        for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
+                expectedCommentLength++) {
+            int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
+            if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
+                int actualCommentLength =
+                        getUnsignedInt16(
+                                zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
+                if (actualCommentLength == expectedCommentLength) {
+                    return eocdStartPos;
+                }
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
+     * Locator.
+     *
+     * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
+     *        in the file.
+     *
+     * @throws IOException if an I/O error occurs while reading the data source
+     */
+    public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
+            DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException {
+
+        // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
+        // Directory Record.
+        long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
+        if (locatorPosition < 0) {
+            return false;
+        }
+
+        ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4);
+        sig.order(ByteOrder.LITTLE_ENDIAN);
+        return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
+    }
+
     private 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) {
+        return buffer.getShort(offset) & 0xffff;
+    }
+
     private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
         if ((value < 0) || (value > 0xffffffffL)) {
             throw new IllegalArgumentException("uint32 value of out range: " + value);
         }
-        buffer.putInt(buffer.position() + offset, (int) value);
+        buffer.putInt(offset, (int) value);
+    }
+
+    private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
+        return buffer.getInt(offset) & 0xffffffffL;
     }
 }
\ No newline at end of file
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
index 27ff7a8..e268dd2 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
@@ -17,6 +17,7 @@
 package com.android.apksigner.core.util;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
 
 /**
  * Abstract representation of a source of data.
@@ -29,6 +30,25 @@
  *     may have worked as the unifying abstraction.</li>
  * <li>Support sources which do not fit into logical memory as a contiguous region.</li>
  * </ul>
+ *
+ * <p>There are following ways to obtain a chunk of data from the data source:
+ * <ul>
+ * <li>Stream the chunk's data into a {@link DataSink} using
+ *     {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no
+ *     need to have the chunk's data accessible at the same time, for example, when computing the
+ *     digest of the chunk. If you need to keep the chunk's data around after {@code feed}
+ *     completes, you must create a copy during {@code feed}. However, in that case the following
+ *     methods of obtaining the chunk's data may be more appropriate.</li>
+ * <li>Obtain a {@link ByteBuffer} containing the chunk's data using
+ *     {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's
+ *     data may or may not be copied by this operation. This is best suited for scenarios where
+ *     you need to access the chunk's data in arbitrary order, but don't need to modify the data and
+ *     thus don't require a copy of the data.</li>
+ * <li>Copy the chunk's data to a {@link ByteBuffer} using
+ *     {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where
+ *     you require a copy of the chunk's data, such as to when you need to modify the data.
+ *     </li>
+ * </ul>
  */
 public interface DataSource {
 
@@ -46,8 +66,33 @@
     void feed(long offset, long size, DataSink sink) throws IOException;
 
     /**
+     * Returns a buffer holding the contents of the specified chunk of data from this data source.
+     * Changes to the data source are not guaranteed to be reflected in the returned buffer.
+     * Similarly, changes in the buffer are not guaranteed to be reflected in the data source.
+     *
+     * <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is
+     * {@code size}.
+     *
+     * @param offset index (in bytes) at which the chunk starts inside data source
+     * @param size size (in bytes) of the chunk
+     */
+    ByteBuffer getByteBuffer(long offset, int size) throws IOException;
+
+    /**
+     * Copies the specified chunk from this data source into the provided destination buffer,
+     * advancing the destination buffer's position by {@code size}.
+     *
+     * @param offset index (in bytes) at which the chunk starts inside data source
+     * @param size size (in bytes) of the chunk
+     */
+    void copyTo(long offset, int size, ByteBuffer dest) throws IOException;
+
+    /**
      * Returns a data source representing the specified region of data of this data source. Changes
      * to data represented by this data source will also be visible in the returned data source.
+     *
+     * @param offset index (in bytes) at which the region starts inside data source
+     * @param size size (in bytes) of the region
      */
     DataSource slice(long offset, long size);
 }
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java
new file mode 100644
index 0000000..7da57d9
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.zip;
+
+/**
+ * Indicates that a ZIP archive is not well-formed.
+ */
+public class ZipFormatException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public ZipFormatException(String message) {
+        super(message);
+    }
+
+    public ZipFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/tools/ijar/Android.bp b/tools/ijar/Android.bp
new file mode 100644
index 0000000..f7e9a28
--- /dev/null
+++ b/tools/ijar/Android.bp
@@ -0,0 +1,20 @@
+// Copyright 2015 The Android Open Source Project
+//
+// The rest of files in this directory comes from
+// https://github.com/bazelbuild/bazel/tree/master/third_party/ijar
+
+cc_binary_host {
+    srcs: [
+        "classfile.cc",
+        "ijar.cc",
+        "zip.cc",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    host_ldlibs: ["-lz"],
+    name: "ijar",
+    // libc++ is not supported for TARGET_BUILD_APPS builds
+    stl: "libstdc++",
+}
diff --git a/tools/ijar/Android.mk b/tools/ijar/Android.mk
deleted file mode 100644
index 8b2a02c..0000000
--- a/tools/ijar/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2015 The Android Open Source Project
-#
-# The rest of files in this directory comes from
-# https://github.com/bazelbuild/bazel/tree/master/third_party/ijar
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_CPP_EXTENSION := .cc
-LOCAL_SRC_FILES := classfile.cc ijar.cc zip.cc
-LOCAL_CFLAGS += -Wall -Werror
-LOCAL_SHARED_LIBRARIES := libz-host
-LOCAL_MODULE := ijar
-# libc++ is not supported for TARGET_BUILD_APPS builds
-LOCAL_CXX_STL := libstdc++
-include $(BUILD_HOST_EXECUTABLE)
diff --git a/tools/rgb2565/Android.mk b/tools/rgb2565/Android.mk
deleted file mode 100644
index 189584d..0000000
--- a/tools/rgb2565/Android.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2008 The Android Open Source Project
-#
-# Android.mk for rgb2565
-#
-
-LOCAL_PATH:= $(call my-dir)
-
-# rgb2565 host tool
-# =========================================================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := to565.c
-
-LOCAL_CFLAGS += -O2 -Wall -Wno-unused-parameter
-LOCAL_MODULE := rgb2565
-
-include $(BUILD_HOST_EXECUTABLE)
diff --git a/tools/rgb2565/to565.c b/tools/rgb2565/to565.c
deleted file mode 100644
index 94d62ef..0000000
--- a/tools/rgb2565/to565.c
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2008 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.
- */
-	
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <string.h>
-
-#define to565(r,g,b)                                            \
-    ((((r) >> 3) << 11) | (((g) >> 2) << 5) | ((b) >> 3))
-
-#define from565_r(x) ((((x) >> 11) & 0x1f) * 255 / 31)
-#define from565_g(x) ((((x) >> 5) & 0x3f) * 255 / 63)
-#define from565_b(x) (((x) & 0x1f) * 255 / 31)
-
-void to_565_raw(void)
-{
-    unsigned char in[3];
-    unsigned short out;
-
-    while(read(0, in, 3) == 3) {
-        out = to565(in[0],in[1],in[2]);
-        write(1, &out, 2);
-    }
-    return;
-}
-
-void to_565_raw_dither(int width)
-{
-    unsigned char in[3];
-    unsigned short out;
-    int i = 0;
-    int e;
-
-    int* error = malloc((width+2) * 3 * sizeof(int));
-    int* next_error = malloc((width+2) * 3 * sizeof(int));
-    memset(error, 0, (width+2) * 3 * sizeof(int));
-    memset(next_error, 0, (width+2) * 3 * sizeof(int));
-    error += 3;        // array goes from [-3..((width+1)*3+2)]
-    next_error += 3;
-
-    while(read(0, in, 3) == 3) {
-        int r = in[0] + error[i*3+0];
-        int rb = (r < 0) ? 0 : ((r > 255) ? 255 : r);
-
-        int g = in[1] + error[i*3+1];
-        int gb = (g < 0) ? 0 : ((g > 255) ? 255 : g);
-
-        int b = in[2] + error[i*3+2];
-        int bb = (b < 0) ? 0 : ((b > 255) ? 255 : b);
-
-        out = to565(rb, gb, bb);
-        write(1, &out, 2);
-
-#define apply_error(ch) {                                                \
-            next_error[(i-1)*3+(ch)] += e * 3 / 16;                      \
-            next_error[(i)*3+(ch)] += e * 5 / 16;                        \
-            next_error[(i+1)*3+(ch)] += e * 1 / 16;                      \
-            error[(i+1)*3+(ch)] += e - ((e*1/16) + (e*3/16) + (e*5/16)); \
-        }
-
-        e = r - from565_r(out);
-        apply_error(0);
-
-        e = g - from565_g(out);
-        apply_error(1);
-
-        e = b - from565_b(out);
-        apply_error(2);
-
-#undef apply_error
-
-        ++i;
-        if (i == width) {
-            // error <- next_error; next_error <- 0
-            int* temp = error; error = next_error; next_error = temp;
-            memset(next_error, 0, (width+1) * 3 * sizeof(int));
-            i = 0;
-        }
-    }
-
-    free(error-3);
-    free(next_error-3);
-
-    return;
-}
-
-void to_565_rle(void)
-{
-    unsigned char in[3];
-    unsigned short last, color, count;
-    unsigned total = 0;
-    count = 0;
-
-    while(read(0, in, 3) == 3) {
-        color = to565(in[0],in[1],in[2]);
-        if (count) {
-            if ((color == last) && (count != 65535)) {
-                count++;
-                continue;
-            } else {
-                write(1, &count, 2);
-                write(1, &last, 2);
-                total += count;
-            }
-        }
-        last = color;
-        count = 1;
-    }
-    if (count) {
-        write(1, &count, 2);
-        write(1, &last, 2);
-        total += count;
-    }
-    fprintf(stderr,"%d pixels\n",total);
-}
-
-int main(int argc, char **argv)
-{
-    if ((argc == 2) && (!strcmp(argv[1],"-rle"))) {
-        to_565_rle();
-    } else {
-        if (argc > 2 && (!strcmp(argv[1], "-w"))) {
-            to_565_raw_dither(atoi(argv[2]));
-        } else {
-            to_565_raw();
-        }
-    }
-    return 0;
-}
diff --git a/tools/signapk/Android.mk b/tools/signapk/Android.mk
index ac217c7..eff066c 100644
--- a/tools/signapk/Android.mk
+++ b/tools/signapk/Android.mk
@@ -21,7 +21,11 @@
 LOCAL_MODULE := signapk
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_JAR_MANIFEST := SignApk.mf
-LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host bouncycastle-bcpkix-host conscrypt-host
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    apksigner-core \
+    bouncycastle-host \
+    bouncycastle-bcpkix-host \
+    conscrypt-host
 LOCAL_REQUIRED_MODULES := libconscrypt_openjdk_jni
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/tools/signapk/src/com/android/signapk/ApkSignerV2.java b/tools/signapk/src/com/android/signapk/ApkSignerV2.java
deleted file mode 100644
index 7b617db..0000000
--- a/tools/signapk/src/com/android/signapk/ApkSignerV2.java
+++ /dev/null
@@ -1,725 +0,0 @@
-/*
- * 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.signapk;
-
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.DigestException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * APK Signature Scheme v2 signer.
- *
- * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
- * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
- * uncompressed contents of ZIP entries.
- */
-public abstract class ApkSignerV2 {
-    /*
-     * The two main goals of APK Signature Scheme v2 are:
-     * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
-     *    cover every byte of the APK being signed.
-     * 2. Enable much faster signature and integrity verification. This is achieved by requiring
-     *    only a minimal amount of APK parsing before the signature is verified, thus completely
-     *    bypassing ZIP entry decompression and by making integrity verification parallelizable by
-     *    employing a hash tree.
-     *
-     * The generated signature block is wrapped into an APK Signing Block and inserted into the
-     * original APK immediately before the start of ZIP Central Directory. This is to ensure that
-     * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
-     * extensibility. For example, a future signature scheme could insert its signatures there as
-     * well. The contract of the APK Signing Block is that all contents outside of the block must be
-     * protected by signatures inside the block.
-     */
-
-    public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
-    public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
-    public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
-    public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
-    public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
-    public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
-    public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
-
-    /**
-     * {@code .SF} file header section attribute indicating that the APK is signed not just with
-     * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
-     * facilitates v2 signature stripping detection.
-     *
-     * <p>The attribute contains a comma-separated set of signature scheme IDs.
-     */
-    public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
-    public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
-
-    private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
-    private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
-
-    private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
-    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
-          new byte[] {
-              0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
-              0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
-          };
-    private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
-    private ApkSignerV2() {}
-
-    /**
-     * Signer configuration.
-     */
-    public static final class SignerConfig {
-        /** Private key. */
-        public PrivateKey privateKey;
-
-        /**
-         * Certificates, with the first certificate containing the public key corresponding to
-         * {@link #privateKey}.
-         */
-        public List<X509Certificate> certificates;
-
-        /**
-         * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
-         */
-        public List<Integer> signatureAlgorithms;
-    }
-
-    /**
-     * Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
-     * consecutive chunks.
-     *
-     * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
-     * of META-INF/*.SF files of APK being signed must contain the
-     * {@code X-Android-APK-Signed: true} attribute.
-     *
-     * @param inputApk contents of the APK to be signed. The APK starts at the current position
-     *        of the buffer and ends at the limit of the buffer.
-     * @param signerConfigs signer configurations, one for each signer.
-     *
-     * @throws ApkParseException if the APK cannot be parsed.
-     * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
-     *         cannot be used in general.
-     * @throws SignatureException if an error occurs when computing digests of generating
-     *         signatures.
-     */
-    public static ByteBuffer[] sign(
-            ByteBuffer inputApk,
-            List<SignerConfig> signerConfigs)
-                    throws ApkParseException, InvalidKeyException, SignatureException {
-        // Slice/create a view in the inputApk to make sure that:
-        // 1. inputApk is what's between position and limit of the original inputApk, and
-        // 2. changes to position, limit, and byte order are not reflected in the original.
-        ByteBuffer originalInputApk = inputApk;
-        inputApk = originalInputApk.slice();
-        inputApk.order(ByteOrder.LITTLE_ENDIAN);
-
-        // Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
-        // Directory is immediately followed by the ZIP End of Central Directory.
-        int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
-        if (eocdOffset == -1) {
-            throw new ApkParseException("Failed to locate ZIP End of Central Directory");
-        }
-        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
-            throw new ApkParseException("ZIP64 format not supported");
-        }
-        inputApk.position(eocdOffset);
-        long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
-        if (centralDirSizeLong > Integer.MAX_VALUE) {
-            throw new ApkParseException(
-                    "ZIP Central Directory size out of range: " + centralDirSizeLong);
-        }
-        int centralDirSize = (int) centralDirSizeLong;
-        long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
-        if (centralDirOffsetLong > Integer.MAX_VALUE) {
-            throw new ApkParseException(
-                    "ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
-        }
-        int centralDirOffset = (int) centralDirOffsetLong;
-        int expectedEocdOffset = centralDirOffset + centralDirSize;
-        if (expectedEocdOffset < centralDirOffset) {
-            throw new ApkParseException(
-                    "ZIP Central Directory extent too large. Offset: " + centralDirOffset
-                            + ", size: " + centralDirSize);
-        }
-        if (eocdOffset != expectedEocdOffset) {
-            throw new ApkParseException(
-                    "ZIP Central Directory not immeiately followed by ZIP End of"
-                            + " Central Directory. CD end: " + expectedEocdOffset
-                            + ", EoCD start: " + eocdOffset);
-        }
-
-        // Create ByteBuffers holding the contents of everything before ZIP Central Directory,
-        // ZIP Central Directory, and ZIP End of Central Directory.
-        inputApk.clear();
-        ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
-        ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
-        // Create a copy of End of Central Directory because we'll need modify its contents later.
-        byte[] eocdBytes = new byte[inputApk.remaining()];
-        inputApk.get(eocdBytes);
-        ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
-        eocd.order(inputApk.order());
-
-        // Figure which which digests to use for APK contents.
-        Set<Integer> contentDigestAlgorithms = new HashSet<>();
-        for (SignerConfig signerConfig : signerConfigs) {
-            for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
-                contentDigestAlgorithms.add(
-                        getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
-            }
-        }
-
-        // Compute digests of APK contents.
-        Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
-        try {
-            contentDigests =
-                    computeContentDigests(
-                            contentDigestAlgorithms,
-                            new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
-        } catch (DigestException e) {
-            throw new SignatureException("Failed to compute digests of APK", e);
-        }
-
-        // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
-        ByteBuffer apkSigningBlock =
-                ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
-
-        // Update Central Directory Offset in End of Central Directory Record. Central Directory
-        // follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
-        centralDirOffset += apkSigningBlock.remaining();
-        eocd.clear();
-        ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
-
-        // Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
-        originalInputApk.position(originalInputApk.limit());
-
-        // Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
-        // Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
-        // Contrary to the name, this does not clear the contents of these ByteBuffer.
-        beforeCentralDir.clear();
-        centralDir.clear();
-        eocd.clear();
-
-        // Insert APK Signing Block immediately before the ZIP Central Directory.
-        return new ByteBuffer[] {
-            beforeCentralDir,
-            apkSigningBlock,
-            centralDir,
-            eocd,
-        };
-    }
-
-    private static Map<Integer, byte[]> computeContentDigests(
-            Set<Integer> digestAlgorithms,
-            ByteBuffer[] contents) throws DigestException {
-        // For each digest algorithm the result is computed as follows:
-        // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
-        //    The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
-        //    No chunks are produced for empty (zero length) segments.
-        // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
-        //    length in bytes (uint32 little-endian) and the chunk's contents.
-        // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
-        //    chunks (uint32 little-endian) and the concatenation of digests of chunks of all
-        //    segments in-order.
-
-        int chunkCount = 0;
-        for (ByteBuffer input : contents) {
-            chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
-        }
-
-        final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
-        for (int digestAlgorithm : digestAlgorithms) {
-            int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
-            byte[] concatenationOfChunkCountAndChunkDigests =
-                    new byte[5 + chunkCount * digestOutputSizeBytes];
-            concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
-            setUnsignedInt32LittleEngian(
-                    chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
-            digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
-        }
-
-        int chunkIndex = 0;
-        byte[] chunkContentPrefix = new byte[5];
-        chunkContentPrefix[0] = (byte) 0xa5;
-        // Optimization opportunity: digests of chunks can be computed in parallel.
-        for (ByteBuffer input : contents) {
-            while (input.hasRemaining()) {
-                int chunkSize =
-                        Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
-                final ByteBuffer chunk = getByteBuffer(input, chunkSize);
-                for (int digestAlgorithm : digestAlgorithms) {
-                    String jcaAlgorithmName =
-                            getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
-                    MessageDigest md;
-                    try {
-                        md = MessageDigest.getInstance(jcaAlgorithmName);
-                    } catch (NoSuchAlgorithmException e) {
-                        throw new DigestException(
-                                jcaAlgorithmName + " MessageDigest not supported", e);
-                    }
-                    // Reset position to 0 and limit to capacity. Position would've been modified
-                    // by the preceding iteration of this loop. NOTE: Contrary to the method name,
-                    // this does not modify the contents of the chunk.
-                    chunk.clear();
-                    setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
-                    md.update(chunkContentPrefix);
-                    md.update(chunk);
-                    byte[] concatenationOfChunkCountAndChunkDigests =
-                            digestsOfChunks.get(digestAlgorithm);
-                    int expectedDigestSizeBytes =
-                            getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
-                    int actualDigestSizeBytes =
-                            md.digest(
-                                    concatenationOfChunkCountAndChunkDigests,
-                                    5 + chunkIndex * expectedDigestSizeBytes,
-                                    expectedDigestSizeBytes);
-                    if (actualDigestSizeBytes != expectedDigestSizeBytes) {
-                        throw new DigestException(
-                                "Unexpected output size of " + md.getAlgorithm()
-                                        + " digest: " + actualDigestSizeBytes);
-                    }
-                }
-                chunkIndex++;
-            }
-        }
-
-        Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
-        for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
-            int digestAlgorithm = entry.getKey();
-            byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
-            String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
-            MessageDigest md;
-            try {
-                md = MessageDigest.getInstance(jcaAlgorithmName);
-            } catch (NoSuchAlgorithmException e) {
-                throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
-            }
-            result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
-        }
-        return result;
-    }
-
-    private static final int getChunkCount(int inputSize, int chunkSize) {
-        return (inputSize + chunkSize - 1) / chunkSize;
-    }
-
-    private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) {
-        result[offset] = (byte) (value & 0xff);
-        result[offset + 1] = (byte) ((value >> 8) & 0xff);
-        result[offset + 2] = (byte) ((value >> 16) & 0xff);
-        result[offset + 3] = (byte) ((value >> 24) & 0xff);
-    }
-
-    private static byte[] generateApkSigningBlock(
-            List<SignerConfig> signerConfigs,
-            Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
-        byte[] apkSignatureSchemeV2Block =
-                generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
-        return generateApkSigningBlock(apkSignatureSchemeV2Block);
-    }
-
-    private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
-        // FORMAT:
-        // uint64:  size (excluding this field)
-        // repeated ID-value pairs:
-        //     uint64:           size (excluding this field)
-        //     uint32:           ID
-        //     (size - 4) bytes: value
-        // uint64:  size (same as the one above)
-        // uint128: magic
-
-        int resultSize =
-                8 // size
-                + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
-                + 8 // size
-                + 16 // magic
-                ;
-        ByteBuffer result = ByteBuffer.allocate(resultSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        long blockSizeFieldValue = resultSize - 8;
-        result.putLong(blockSizeFieldValue);
-
-        long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
-        result.putLong(pairSizeFieldValue);
-        result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
-        result.put(apkSignatureSchemeV2Block);
-
-        result.putLong(blockSizeFieldValue);
-        result.put(APK_SIGNING_BLOCK_MAGIC);
-
-        return result.array();
-    }
-
-    private static byte[] generateApkSignatureSchemeV2Block(
-            List<SignerConfig> signerConfigs,
-            Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
-        // FORMAT:
-        // * length-prefixed sequence of length-prefixed signer blocks.
-
-        List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
-        int signerNumber = 0;
-        for (SignerConfig signerConfig : signerConfigs) {
-            signerNumber++;
-            byte[] signerBlock;
-            try {
-                signerBlock = generateSignerBlock(signerConfig, contentDigests);
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
-            } catch (SignatureException e) {
-                throw new SignatureException("Signer #" + signerNumber + " failed", e);
-            }
-            signerBlocks.add(signerBlock);
-        }
-
-        return encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
-                });
-    }
-
-    private static byte[] generateSignerBlock(
-            SignerConfig signerConfig,
-            Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
-        if (signerConfig.certificates.isEmpty()) {
-            throw new SignatureException("No certificates configured for signer");
-        }
-        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
-        byte[] encodedPublicKey = encodePublicKey(publicKey);
-
-        V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
-        try {
-            signedData.certificates = encodeCertificates(signerConfig.certificates);
-        } catch (CertificateEncodingException e) {
-            throw new SignatureException("Failed to encode certificates", e);
-        }
-
-        List<Pair<Integer, byte[]>> digests =
-                new ArrayList<>(signerConfig.signatureAlgorithms.size());
-        for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
-            int contentDigestAlgorithm =
-                    getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
-            byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
-            if (contentDigest == null) {
-                throw new RuntimeException(
-                        getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
-                        + " content digest for "
-                        + getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
-                        + " not computed");
-            }
-            digests.add(Pair.create(signatureAlgorithm, contentDigest));
-        }
-        signedData.digests = digests;
-
-        V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
-        // FORMAT:
-        // * length-prefixed sequence of length-prefixed digests:
-        //   * uint32: signature algorithm ID
-        //   * length-prefixed bytes: digest of contents
-        // * length-prefixed sequence of certificates:
-        //   * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
-        // * length-prefixed sequence of length-prefixed additional attributes:
-        //   * uint32: ID
-        //   * (length - 4) bytes: value
-        signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
-            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
-            encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
-            // additional attributes
-            new byte[0],
-        });
-        signer.publicKey = encodedPublicKey;
-        signer.signatures = new ArrayList<>();
-        for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
-            Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
-                    getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
-            String jcaSignatureAlgorithm = signatureParams.getFirst();
-            AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
-            byte[] signatureBytes;
-            try {
-                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
-                signature.initSign(signerConfig.privateKey);
-                if (jcaSignatureAlgorithmParams != null) {
-                    signature.setParameter(jcaSignatureAlgorithmParams);
-                }
-                signature.update(signer.signedData);
-                signatureBytes = signature.sign();
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
-            } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
-                    | SignatureException e) {
-                throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
-            }
-
-            try {
-                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
-                signature.initVerify(publicKey);
-                if (jcaSignatureAlgorithmParams != null) {
-                    signature.setParameter(jcaSignatureAlgorithmParams);
-                }
-                signature.update(signer.signedData);
-                if (!signature.verify(signatureBytes)) {
-                    throw new SignatureException("Signature did not verify");
-                }
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
-                        + " signature using public key from certificate", e);
-            } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
-                    | SignatureException e) {
-                throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
-                        + " signature using public key from certificate", e);
-            }
-
-            signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
-        }
-
-        // FORMAT:
-        // * length-prefixed signed data
-        // * length-prefixed sequence of length-prefixed signatures:
-        //   * uint32: signature algorithm ID
-        //   * length-prefixed bytes: signature of signed data
-        // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
-        return encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    signer.signedData,
-                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
-                            signer.signatures),
-                    signer.publicKey,
-                });
-    }
-
-    private static final class V2SignatureSchemeBlock {
-        private static final class Signer {
-            public byte[] signedData;
-            public List<Pair<Integer, byte[]>> signatures;
-            public byte[] publicKey;
-        }
-
-        private static final class SignedData {
-            public List<Pair<Integer, byte[]>> digests;
-            public List<byte[]> certificates;
-        }
-    }
-
-    private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
-        byte[] encodedPublicKey = null;
-        if ("X.509".equals(publicKey.getFormat())) {
-            encodedPublicKey = publicKey.getEncoded();
-        }
-        if (encodedPublicKey == null) {
-            try {
-                encodedPublicKey =
-                        KeyFactory.getInstance(publicKey.getAlgorithm())
-                                .getKeySpec(publicKey, X509EncodedKeySpec.class)
-                                .getEncoded();
-            } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
-                throw new InvalidKeyException(
-                        "Failed to obtain X.509 encoded form of public key " + publicKey
-                                + " of class " + publicKey.getClass().getName(),
-                        e);
-            }
-        }
-        if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
-            throw new InvalidKeyException(
-                    "Failed to obtain X.509 encoded form of public key " + publicKey
-                            + " of class " + publicKey.getClass().getName());
-        }
-        return encodedPublicKey;
-    }
-
-    public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
-            throws CertificateEncodingException {
-        List<byte[]> result = new ArrayList<>();
-        for (X509Certificate certificate : certificates) {
-            result.add(certificate.getEncoded());
-        }
-        return result;
-    }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
-        return encodeAsSequenceOfLengthPrefixedElements(
-                sequence.toArray(new byte[sequence.size()][]));
-    }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
-        int payloadSize = 0;
-        for (byte[] element : sequence) {
-            payloadSize += 4 + element.length;
-        }
-        ByteBuffer result = ByteBuffer.allocate(payloadSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        for (byte[] element : sequence) {
-            result.putInt(element.length);
-            result.put(element);
-        }
-        return result.array();
-      }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
-            List<Pair<Integer, byte[]>> sequence) {
-        int resultSize = 0;
-        for (Pair<Integer, byte[]> element : sequence) {
-            resultSize += 12 + element.getSecond().length;
-        }
-        ByteBuffer result = ByteBuffer.allocate(resultSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        for (Pair<Integer, byte[]> element : sequence) {
-            byte[] second = element.getSecond();
-            result.putInt(8 + second.length);
-            result.putInt(element.getFirst());
-            result.putInt(second.length);
-            result.put(second);
-        }
-        return result.array();
-    }
-
-    /**
-     * Relative <em>get</em> method for reading {@code size} number of bytes from the current
-     * position of this buffer.
-     *
-     * <p>This method reads the next {@code size} bytes at this buffer's current position,
-     * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
-     * {@code size}, byte order set to this buffer's byte order; and then increments the position by
-     * {@code size}.
-     */
-    private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
-        if (size < 0) {
-            throw new IllegalArgumentException("size: " + size);
-        }
-        int originalLimit = source.limit();
-        int position = source.position();
-        int limit = position + size;
-        if ((limit < position) || (limit > originalLimit)) {
-            throw new BufferUnderflowException();
-        }
-        source.limit(limit);
-        try {
-            ByteBuffer result = source.slice();
-            result.order(source.order());
-            source.position(limit);
-            return result;
-        } finally {
-            source.limit(originalLimit);
-        }
-    }
-
-    private static Pair<String, ? extends AlgorithmParameterSpec>
-            getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
-        switch (sigAlgorithm) {
-            case SIGNATURE_RSA_PSS_WITH_SHA256:
-                return Pair.create(
-                        "SHA256withRSA/PSS",
-                        new PSSParameterSpec(
-                                "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
-            case SIGNATURE_RSA_PSS_WITH_SHA512:
-                return Pair.create(
-                        "SHA512withRSA/PSS",
-                        new PSSParameterSpec(
-                                "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
-            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
-                return Pair.create("SHA256withRSA", null);
-            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
-                return Pair.create("SHA512withRSA", null);
-            case SIGNATURE_ECDSA_WITH_SHA256:
-                return Pair.create("SHA256withECDSA", null);
-            case SIGNATURE_ECDSA_WITH_SHA512:
-                return Pair.create("SHA512withECDSA", null);
-            case SIGNATURE_DSA_WITH_SHA256:
-                return Pair.create("SHA256withDSA", null);
-            default:
-                throw new IllegalArgumentException(
-                        "Unknown signature algorithm: 0x"
-                                + Long.toHexString(sigAlgorithm & 0xffffffff));
-        }
-    }
-
-    private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
-        switch (sigAlgorithm) {
-            case SIGNATURE_RSA_PSS_WITH_SHA256:
-            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
-            case SIGNATURE_ECDSA_WITH_SHA256:
-            case SIGNATURE_DSA_WITH_SHA256:
-                return CONTENT_DIGEST_CHUNKED_SHA256;
-            case SIGNATURE_RSA_PSS_WITH_SHA512:
-            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
-            case SIGNATURE_ECDSA_WITH_SHA512:
-                return CONTENT_DIGEST_CHUNKED_SHA512;
-            default:
-                throw new IllegalArgumentException(
-                        "Unknown signature algorithm: 0x"
-                                + Long.toHexString(sigAlgorithm & 0xffffffff));
-        }
-    }
-
-    private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
-        switch (digestAlgorithm) {
-            case CONTENT_DIGEST_CHUNKED_SHA256:
-                return "SHA-256";
-            case CONTENT_DIGEST_CHUNKED_SHA512:
-                return "SHA-512";
-            default:
-                throw new IllegalArgumentException(
-                        "Unknown content digest algorthm: " + digestAlgorithm);
-        }
-    }
-
-    private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
-        switch (digestAlgorithm) {
-            case CONTENT_DIGEST_CHUNKED_SHA256:
-                return 256 / 8;
-            case CONTENT_DIGEST_CHUNKED_SHA512:
-                return 512 / 8;
-            default:
-                throw new IllegalArgumentException(
-                        "Unknown content digest algorthm: " + digestAlgorithm);
-        }
-    }
-
-    /**
-     * Indicates that APK file could not be parsed.
-     */
-    public static class ApkParseException extends Exception {
-        private static final long serialVersionUID = 1L;
-
-        public ApkParseException(String message) {
-            super(message);
-        }
-
-        public ApkParseException(String message, Throwable cause) {
-            super(message, cause);
-        }
-    }
-}
diff --git a/tools/signapk/src/com/android/signapk/Pair.java b/tools/signapk/src/com/android/signapk/Pair.java
deleted file mode 100644
index e4a6c92..0000000
--- a/tools/signapk/src/com/android/signapk/Pair.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.signapk;
-
-/**
- * Pair of two elements.
- */
-public final class Pair<A, B> {
-    private final A mFirst;
-    private final B mSecond;
-
-    private Pair(A first, B second) {
-        mFirst = first;
-        mSecond = second;
-    }
-
-    public static <A, B> Pair<A, B> create(A first, B second) {
-        return new Pair<A, B>(first, second);
-    }
-
-    public A getFirst() {
-        return mFirst;
-    }
-
-    public B getSecond() {
-        return mSecond;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
-        result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        @SuppressWarnings("rawtypes")
-        Pair other = (Pair) obj;
-        if (mFirst == null) {
-            if (other.mFirst != null) {
-                return false;
-            }
-        } else if (!mFirst.equals(other.mFirst)) {
-            return false;
-        }
-        if (mSecond == null) {
-            if (other.mSecond != null) {
-                return false;
-            }
-        } else if (!mSecond.equals(other.mSecond)) {
-            return false;
-        }
-        return true;
-    }
-}
diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java
index a7c9fc3..d84d070 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -23,7 +23,6 @@
 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
 import org.bouncycastle.cert.jcajce.JcaCertStore;
 import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.CMSProcessableByteArray;
 import org.bouncycastle.cms.CMSSignedData;
 import org.bouncycastle.cms.CMSSignedDataGenerator;
 import org.bouncycastle.cms.CMSTypedData;
@@ -33,9 +32,15 @@
 import org.bouncycastle.operator.OperatorCreationException;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
-import org.bouncycastle.util.encoders.Base64;
 import org.conscrypt.OpenSSLProvider;
 
+import com.android.apksigner.core.ApkSignerEngine;
+import com.android.apksigner.core.DefaultApkSignerEngine;
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSources;
+import com.android.apksigner.core.zip.ZipFormatException;
+
 import java.io.Console;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
@@ -49,19 +54,14 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.PrintStream;
 import java.lang.reflect.Constructor;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.security.DigestOutputStream;
 import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
 import java.security.Key;
 import java.security.KeyFactory;
-import java.security.MessageDigest;
 import java.security.PrivateKey;
 import java.security.Provider;
-import java.security.PublicKey;
 import java.security.Security;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateFactory;
@@ -71,17 +71,12 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
 import java.util.regex.Pattern;
 
 import javax.crypto.Cipher;
@@ -113,11 +108,6 @@
  * APK Signature Scheme v2.
  */
 class SignApk {
-    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
-    private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
-    private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
-    private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
-
     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
 
     /**
@@ -137,36 +127,6 @@
     private static final int USE_SHA1 = 1;
     private static final int USE_SHA256 = 2;
 
-    /** Digest algorithm used when signing the APK using APK Signature Scheme v2. */
-    private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
-
-    /**
-     * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
-     * for v1 signing (JAR signing) an APK using the private key corresponding to the provided
-     * certificate.
-     *
-     * @param minSdkVersion minimum Android platform API Level supported by the APK (see
-     *        minSdkVersion attribute in AndroidManifest.xml). The higher the minSdkVersion, the
-     *        stronger hash may be used for signing the APK.
-     */
-    private static int getV1DigestAlgorithmForApk(X509Certificate cert, int minSdkVersion) {
-        String keyAlgorithm = cert.getPublicKey().getAlgorithm();
-        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-            // RSA can be used only with SHA-1 prior to API Level 18.
-            return (minSdkVersion < 18) ? USE_SHA1 : USE_SHA256;
-        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-            // ECDSA cannot be used prior to API Level 18 at all. It can only be used with SHA-1
-            // on API Levels 18, 19, and 20.
-            if (minSdkVersion < 18) {
-                throw new IllegalArgumentException(
-                        "ECDSA signatures only supported for minSdkVersion 18 and higher");
-            }
-            return (minSdkVersion < 21) ? USE_SHA1 : USE_SHA256;
-        } else {
-            throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
-        }
-    }
-
     /**
      * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
      * for signing an OTA update package using the private key corresponding to the provided
@@ -187,10 +147,10 @@
 
     /**
      * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
-     * or v1 signing an APK using the private key corresponding to the provided certificate and the
+     * update package using the private key corresponding to the provided certificate and the
      * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
      */
-    private static String getJcaSignatureAlgorithmForV1SigningOrOta(
+    private static String getJcaSignatureAlgorithmForOta(
             X509Certificate cert, int hash) {
         String sigAlgDigestPrefix;
         switch (hash) {
@@ -214,11 +174,6 @@
         }
     }
 
-    /* Files matching this pattern are not copied to the output. */
-    private static final Pattern STRIP_PATTERN =
-            Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
-                    + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
-
     private static X509Certificate readPublicKey(File file)
         throws IOException, GeneralSecurityException {
         FileInputStream input = new FileInputStream(file);
@@ -324,89 +279,6 @@
     }
 
     /**
-     * Add the hash(es) of every file to the manifest, creating it if
-     * necessary.
-     */
-    private static Manifest addDigestsToManifest(
-            JarFile jar, Pattern ignoredFilenamePattern, int hashes)
-                    throws IOException, GeneralSecurityException {
-        Manifest input = jar.getManifest();
-        Manifest output = new Manifest();
-        Attributes main = output.getMainAttributes();
-        if (input != null) {
-            main.putAll(input.getMainAttributes());
-        } else {
-            main.putValue("Manifest-Version", "1.0");
-            main.putValue("Created-By", "1.0 (Android SignApk)");
-        }
-
-        MessageDigest md_sha1 = null;
-        MessageDigest md_sha256 = null;
-        if ((hashes & USE_SHA1) != 0) {
-            md_sha1 = MessageDigest.getInstance("SHA1");
-        }
-        if ((hashes & USE_SHA256) != 0) {
-            md_sha256 = MessageDigest.getInstance("SHA256");
-        }
-
-        byte[] buffer = new byte[4096];
-        int num;
-
-        // We sort the input entries by name, and add them to the
-        // output manifest in sorted order.  We expect that the output
-        // map will be deterministic.
-
-        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
-
-        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
-            JarEntry entry = e.nextElement();
-            byName.put(entry.getName(), entry);
-        }
-
-        for (JarEntry entry: byName.values()) {
-            String name = entry.getName();
-            if (!entry.isDirectory()
-                    && (ignoredFilenamePattern == null
-                            || !ignoredFilenamePattern.matcher(name).matches())) {
-                InputStream data = jar.getInputStream(entry);
-                while ((num = data.read(buffer)) > 0) {
-                    if (md_sha1 != null) md_sha1.update(buffer, 0, num);
-                    if (md_sha256 != null) md_sha256.update(buffer, 0, num);
-                }
-
-                Attributes attr = null;
-                if (input != null) attr = input.getAttributes(name);
-                attr = attr != null ? new Attributes(attr) : new Attributes();
-                // Remove any previously computed digests from this entry's attributes.
-                for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext();) {
-                    Object key = i.next();
-                    if (!(key instanceof Attributes.Name)) {
-                        continue;
-                    }
-                    String attributeNameLowerCase =
-                            ((Attributes.Name) key).toString().toLowerCase(Locale.US);
-                    if (attributeNameLowerCase.endsWith("-digest")) {
-                        i.remove();
-                    }
-                }
-                // Add SHA-1 digest if requested
-                if (md_sha1 != null) {
-                    attr.putValue("SHA1-Digest",
-                                  new String(Base64.encode(md_sha1.digest()), "ASCII"));
-                }
-                // Add SHA-256 digest if requested
-                if (md_sha256 != null) {
-                    attr.putValue("SHA-256-Digest",
-                                  new String(Base64.encode(md_sha256.digest()), "ASCII"));
-                }
-                output.getEntries().put(name, attr);
-            }
-        }
-
-        return output;
-    }
-
-    /**
      * Add a copy of the public key to the archive; this should
      * exactly match one of the files in
      * /system/etc/security/otacerts.zip on the device.  (The same
@@ -416,7 +288,7 @@
     private static void addOtacert(JarOutputStream outputJar,
                                    File publicKeyFile,
                                    long timestamp)
-        throws IOException, GeneralSecurityException {
+        throws IOException {
 
         JarEntry je = new JarEntry(OTACERT_NAME);
         je.setTime(timestamp);
@@ -431,94 +303,6 @@
     }
 
 
-    /** Write to another stream and track how many bytes have been
-     *  written.
-     */
-    private static class CountOutputStream extends FilterOutputStream {
-        private int mCount;
-
-        public CountOutputStream(OutputStream out) {
-            super(out);
-            mCount = 0;
-        }
-
-        @Override
-        public void write(int b) throws IOException {
-            super.write(b);
-            mCount++;
-        }
-
-        @Override
-        public void write(byte[] b, int off, int len) throws IOException {
-            super.write(b, off, len);
-            mCount += len;
-        }
-
-        public int size() {
-            return mCount;
-        }
-    }
-
-    /** Write a .SF file with a digest of the specified manifest. */
-    private static void writeSignatureFile(Manifest manifest, OutputStream out,
-            int hash, boolean additionallySignedUsingAnApkSignatureScheme)
-        throws IOException, GeneralSecurityException {
-        Manifest sf = new Manifest();
-        Attributes main = sf.getMainAttributes();
-        main.putValue("Signature-Version", "1.0");
-        main.putValue("Created-By", "1.0 (Android SignApk)");
-        if (additionallySignedUsingAnApkSignatureScheme) {
-            // Add APK Signature Scheme v2 signature stripping protection.
-            // This attribute indicates that this APK is supposed to have been signed using one or
-            // more APK-specific signature schemes in addition to the standard JAR signature scheme
-            // used by this code. APK signature verifier should reject the APK if it does not
-            // contain a signature for the signature scheme the verifier prefers out of this set.
-            main.putValue(
-                    ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
-                    ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
-        }
-
-        MessageDigest md = MessageDigest.getInstance(
-            hash == USE_SHA256 ? "SHA256" : "SHA1");
-        PrintStream print = new PrintStream(
-            new DigestOutputStream(new ByteArrayOutputStream(), md),
-            true, "UTF-8");
-
-        // Digest of the entire manifest
-        manifest.write(print);
-        print.flush();
-        main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
-                      new String(Base64.encode(md.digest()), "ASCII"));
-
-        Map<String, Attributes> entries = manifest.getEntries();
-        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
-            // Digest of the manifest stanza for this entry.
-            print.print("Name: " + entry.getKey() + "\r\n");
-            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
-                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
-            }
-            print.print("\r\n");
-            print.flush();
-
-            Attributes sfAttr = new Attributes();
-            sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
-                            new String(Base64.encode(md.digest()), "ASCII"));
-            sf.getEntries().put(entry.getKey(), sfAttr);
-        }
-
-        CountOutputStream cout = new CountOutputStream(out);
-        sf.write(cout);
-
-        // A bug in the java.util.jar implementation of Android platforms
-        // up to version 1.6 will cause a spurious IOException to be thrown
-        // if the length of the signature file is a multiple of 1024 bytes.
-        // As a workaround, add an extra CRLF in this case.
-        if ((cout.size() % 1024) == 0) {
-            cout.write('\r');
-            cout.write('\n');
-        }
-    }
-
     /** Sign data and write the digital signature to 'out'. */
     private static void writeSignatureBlock(
         CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
@@ -534,7 +318,7 @@
         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
         ContentSigner signer =
                 new JcaContentSignerBuilder(
-                        getJcaSignatureAlgorithmForV1SigningOrOta(publicKey, hash))
+                        getJcaSignatureAlgorithmForOta(publicKey, hash))
                         .build(privateKey);
         gen.addSignerInfoGenerator(
             new JcaSignerInfoGeneratorBuilder(
@@ -552,12 +336,39 @@
     }
 
     /**
+     * Adds ZIP entries which represent the v1 signature (JAR signature scheme).
+     */
+    private static void addV1Signature(
+            ApkSignerEngine apkSigner,
+            ApkSignerEngine.OutputJarSignatureRequest v1Signature,
+            JarOutputStream out,
+            long timestamp) throws IOException {
+        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
+                : v1Signature.getAdditionalJarEntries()) {
+            String entryName = entry.getName();
+            JarEntry outEntry = new JarEntry(entryName);
+            outEntry.setTime(timestamp);
+            out.putNextEntry(outEntry);
+            byte[] entryData = entry.getData();
+            out.write(entryData);
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                    apkSigner.outputJarEntry(entryName);
+            if (inspectEntryRequest != null) {
+                inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
+                inspectEntryRequest.done();
+            }
+        }
+    }
+
+    /**
      * Copy all JAR entries from input to output. We set the modification times in the output to a
      * fixed time, so as to reduce variation in the output file and make incremental OTAs more
      * efficient.
      */
-    private static void copyFiles(JarFile in,
+    private static void copyFiles(
+            JarFile in,
             Pattern ignoredFilenamePattern,
+            ApkSignerEngine apkSigner,
             JarOutputStream out,
             long timestamp,
             int defaultAlignment) throws IOException {
@@ -589,12 +400,21 @@
         // the start of the file and makes it easier to do alignment
         // on them (since only stored entries are aligned).
 
+        List<String> remainingNames = new ArrayList<>(names.size());
         for (String name : names) {
             JarEntry inEntry = in.getJarEntry(name);
-            JarEntry outEntry = null;
-            if (inEntry.getMethod() != JarEntry.STORED) continue;
+            if (inEntry.getMethod() != JarEntry.STORED) {
+                // Defer outputting this entry until we're ready to output compressed entries.
+                remainingNames.add(name);
+                continue;
+            }
+
+            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
+                continue;
+            }
+
             // Preserve the STORED method of the input entry.
-            outEntry = new JarEntry(inEntry);
+            JarEntry outEntry = new JarEntry(inEntry);
             outEntry.setTime(timestamp);
             // Discard comment and extra fields of this entry to
             // simplify alignment logic below and for consistency with
@@ -638,33 +458,97 @@
             offset += extra.length;
 
             out.putNextEntry(outEntry);
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
+            DataSink entryDataSink =
+                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
 
-            InputStream data = in.getInputStream(inEntry);
-            while ((num = data.read(buffer)) > 0) {
-                out.write(buffer, 0, num);
-                offset += num;
+            try (InputStream data = in.getInputStream(inEntry)) {
+                while ((num = data.read(buffer)) > 0) {
+                    out.write(buffer, 0, num);
+                    if (entryDataSink != null) {
+                        entryDataSink.consume(buffer, 0, num);
+                    }
+                    offset += num;
+                }
             }
             out.flush();
+            if (inspectEntryRequest != null) {
+                inspectEntryRequest.done();
+            }
         }
 
         // Copy all the non-STORED entries.  We don't attempt to
         // maintain the 'offset' variable past this point; we don't do
         // alignment on these entries.
 
-        for (String name : names) {
+        for (String name : remainingNames) {
             JarEntry inEntry = in.getJarEntry(name);
-            JarEntry outEntry = null;
-            if (inEntry.getMethod() == JarEntry.STORED) continue;
+            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
+                continue;
+            }
+
             // Create a new entry so that the compressed len is recomputed.
-            outEntry = new JarEntry(name);
+            JarEntry outEntry = new JarEntry(name);
             outEntry.setTime(timestamp);
             out.putNextEntry(outEntry);
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
+            DataSink entryDataSink =
+                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
 
             InputStream data = in.getInputStream(inEntry);
             while ((num = data.read(buffer)) > 0) {
                 out.write(buffer, 0, num);
+                if (entryDataSink != null) {
+                    entryDataSink.consume(buffer, 0, num);
+                }
             }
             out.flush();
+            if (inspectEntryRequest != null) {
+                inspectEntryRequest.done();
+            }
+        }
+    }
+
+    private static boolean shouldOutputApkEntry(
+            ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
+                    throws IOException {
+        if (apkSigner == null) {
+            return true;
+        }
+
+        ApkSignerEngine.InputJarEntryInstructions instructions =
+                apkSigner.inputJarEntry(inEntry.getName());
+        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                instructions.getInspectJarEntryRequest();
+        if (inspectEntryRequest != null) {
+            provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
+        }
+        switch (instructions.getOutputPolicy()) {
+            case OUTPUT:
+                return true;
+            case SKIP:
+            case OUTPUT_BY_ENGINE:
+                return false;
+            default:
+                throw new RuntimeException(
+                        "Unsupported output policy: " + instructions.getOutputPolicy());
+        }
+    }
+
+    private static void provideJarEntry(
+            JarFile jarFile,
+            JarEntry jarEntry,
+            ApkSignerEngine.InspectJarEntryRequest request,
+            byte[] tmpbuf) throws IOException {
+        DataSink dataSink = request.getDataSink();
+        try (InputStream in = jarFile.getInputStream(jarEntry)) {
+            int chunkSize;
+            while ((chunkSize = in.read(tmpbuf)) > 0) {
+                dataSink.consume(tmpbuf, 0, chunkSize);
+            }
+            request.done();
         }
     }
 
@@ -756,6 +640,11 @@
         private final ASN1ObjectIdentifier type;
         private WholeFileSignerOutputStream signer;
 
+        // Files matching this pattern are not copied to the output.
+        private static final Pattern STRIP_PATTERN =
+                Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
+                        + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
+
         public CMSSigner(JarFile inputJar, File publicKeyFile,
                          X509Certificate publicKey, PrivateKey privateKey, int hash,
                          long timestamp, OutputStream outputStream) {
@@ -789,7 +678,7 @@
                 signer = new WholeFileSignerOutputStream(out, outputStream);
                 JarOutputStream outputJar = new JarOutputStream(signer);
 
-                copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, 0);
+                copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0);
                 addOtacert(outputJar, publicKeyFile, timestamp);
 
                 signer.notifyClosing();
@@ -883,47 +772,6 @@
         temp.writeTo(outputStream);
     }
 
-    private static void signFile(Manifest manifest,
-                                 X509Certificate[] publicKey, PrivateKey[] privateKey, int[] hash,
-                                 long timestamp,
-                                 boolean additionallySignedUsingAnApkSignatureScheme,
-                                 JarOutputStream outputJar)
-        throws Exception {
-
-        // MANIFEST.MF
-        JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
-        je.setTime(timestamp);
-        outputJar.putNextEntry(je);
-        manifest.write(outputJar);
-
-        int numKeys = publicKey.length;
-        for (int k = 0; k < numKeys; ++k) {
-            // CERT.SF / CERT#.SF
-            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
-                              (String.format(CERT_SF_MULTI_NAME, k)));
-            je.setTime(timestamp);
-            outputJar.putNextEntry(je);
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            writeSignatureFile(
-                    manifest,
-                    baos,
-                    hash[k],
-                    additionallySignedUsingAnApkSignatureScheme);
-            byte[] signedData = baos.toByteArray();
-            outputJar.write(signedData);
-
-            // CERT.{EC,RSA} / CERT#.{EC,RSA}
-            final String keyType = publicKey[k].getPublicKey().getAlgorithm();
-            je = new JarEntry(numKeys == 1 ?
-                              (String.format(CERT_SIG_NAME, keyType)) :
-                              (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
-            je.setTime(timestamp);
-            outputJar.putNextEntry(je);
-            writeSignatureBlock(new CMSProcessableByteArray(signedData),
-                                publicKey[k], privateKey[k], hash[k], outputJar);
-        }
-    }
-
     /**
      * Tries to load a JSE Provider by class name. This is for custom PrivateKey
      * types that might be stored in PKCS#11-like storage.
@@ -976,81 +824,68 @@
         Security.insertProviderAt((Provider) o, 1);
     }
 
-    /**
-     * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
-     * into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
-     */
-    public static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
-            PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
-                    throws InvalidKeyException {
+    private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
+            PrivateKey[] privateKeys, X509Certificate[] certificates) {
         if (privateKeys.length != certificates.length) {
             throw new IllegalArgumentException(
                     "The number of private keys must match the number of certificates: "
                             + privateKeys.length + " vs" + certificates.length);
         }
-        List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
+        List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
+        String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
         for (int i = 0; i < privateKeys.length; i++) {
-            PrivateKey privateKey = privateKeys[i];
-            X509Certificate certificate = certificates[i];
-            PublicKey publicKey = certificate.getPublicKey();
-            String keyAlgorithm = privateKey.getAlgorithm();
-            if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
-                throw new InvalidKeyException(
-                        "Key algorithm of private key #" + (i + 1) + " does not match key"
-                        + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
-                        + " vs " + publicKey.getAlgorithm());
-            }
-            ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
-            signerConfig.privateKey = privateKey;
-            signerConfig.certificates = Collections.singletonList(certificate);
-            List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
-            for (String digestAlgorithm : digestAlgorithms) {
-                try {
-                    signatureAlgorithms.add(
-                            getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
-                } catch (IllegalArgumentException e) {
-                    throw new InvalidKeyException(
-                            "Unsupported key and digest algorithm combination for signer #"
-                                    + (i + 1),
-                            e);
-                }
-            }
-            signerConfig.signatureAlgorithms = signatureAlgorithms;
-            result.add(signerConfig);
+            String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
+            DefaultApkSignerEngine.SignerConfig signerConfig =
+                    new DefaultApkSignerEngine.SignerConfig.Builder(
+                            signerName,
+                            privateKeys[i],
+                            Collections.singletonList(certificates[i]))
+                            .build();
+            signerConfigs.add(signerConfig);
         }
-        return result;
+        return signerConfigs;
     }
 
-    private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
-        if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
-            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
-                // deterministic signatures which make life easier for OTA updates (fewer files
-                // changed when deterministic signature schemes are used).
-                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
-            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
-            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-                return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
-            } else {
-                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
-            }
-        } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
-            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
-                // deterministic signatures which make life easier for OTA updates (fewer files
-                // changed when deterministic signature schemes are used).
-                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
-            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
-            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-                throw new IllegalArgumentException("SHA-512 is not supported with DSA");
-            } else {
-                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
-            }
-        } else {
-            throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+    private static class ZipSections {
+        ByteBuffer beforeCentralDir;
+        ByteBuffer centralDir;
+        ByteBuffer eocd;
+    }
+
+    private static ZipSections findMainZipSections(ByteBuffer apk)
+            throws IOException, ZipFormatException {
+        apk.slice();
+        ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
+        long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
+        long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
+        long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
+        long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
+        if (centralDirEndOffset != eocdStartOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory is not immediately followed by End of Central Directory"
+                            + ". CD end: " + centralDirEndOffset
+                            + ", EoCD start: " + eocdStartOffset);
         }
+        apk.position(0);
+        apk.limit((int) centralDirStartOffset);
+        ByteBuffer beforeCentralDir = apk.slice();
+
+        apk.position((int) centralDirStartOffset);
+        apk.limit((int) centralDirEndOffset);
+        ByteBuffer centralDir = apk.slice();
+
+        apk.position((int) eocdStartOffset);
+        apk.limit(apk.capacity());
+        ByteBuffer eocd = apk.slice();
+
+        apk.position(0);
+        apk.limit(apk.capacity());
+
+        ZipSections result = new ZipSections();
+        result.beforeCentralDir = beforeCentralDir;
+        result.centralDir = centralDir;
+        result.eocd = eocd;
+        return result;
     }
 
     private static void usage() {
@@ -1167,57 +1002,80 @@
                         timestamp,
                         outputFile);
             } else {
-                // Generate, in memory, an APK signed using standard JAR Signature Scheme.
-                ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
-                JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
-                // Use maximum compression for compressed entries because the APK lives forever on
-                // the system partition.
-                outputJar.setLevel(9);
-                int v1DigestAlgorithmBitSet = 0;
-                int[] v1DigestAlgorithm = new int[numKeys];
-                for (int i = 0; i < numKeys; ++i) {
-                    v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion);
-                    v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i];
-                }
-                Manifest manifest =
-                        addDigestsToManifest(inputJar, STRIP_PATTERN, v1DigestAlgorithmBitSet);
-                copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, alignment);
-                signFile(
-                        manifest,
-                        publicKey, privateKey, v1DigestAlgorithm,
-                        timestamp, signUsingApkSignatureSchemeV2,
-                        outputJar);
-                outputJar.close();
-                ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
-                v1SignedApkBuf.reset();
+                try (ApkSignerEngine apkSigner =
+                        new DefaultApkSignerEngine.Builder(
+                                createSignerConfigs(privateKey, publicKey), minSdkVersion)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
+                                .setOtherSignersSignaturesPreserved(false)
+                                .build()) {
+                    // We don't preserve the input APK's APK Signing Block (which contains v2
+                    // signatures)
+                    apkSigner.inputApkSigningBlock(null);
 
-                ByteBuffer[] outputChunks;
-                if (signUsingApkSignatureSchemeV2) {
-                    // Additionally sign the APK using the APK Signature Scheme v2.
-                    ByteBuffer apkContents = v1SignedApk;
-                    List<ApkSignerV2.SignerConfig> signerConfigs =
-                            createV2SignerConfigs(
-                                    privateKey,
-                                    publicKey,
-                                    new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
-                    outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
-                } else {
-                    // Output the JAR-signed APK as is.
-                    outputChunks = new ByteBuffer[] {v1SignedApk};
+                    // Build the output APK in memory, by copying input APK's ZIP entries across
+                    // and then signing the output APK.
+                    ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
+                    JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
+                    // Use maximum compression for compressed entries because the APK lives forever
+                    // on the system partition.
+                    outputJar.setLevel(9);
+                    copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment);
+                    ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
+                            apkSigner.outputJarEntries();
+                    if (addV1SignatureRequest != null) {
+                        addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
+                        addV1SignatureRequest.done();
+                    }
+                    outputJar.close();
+                    ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
+                    v1SignedApkBuf.reset();
+                    ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
+
+                    ZipSections zipSections = findMainZipSections(v1SignedApk);
+                    ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest =
+                            apkSigner.outputZipSections(
+                                    DataSources.asDataSource(zipSections.beforeCentralDir),
+                                    DataSources.asDataSource(zipSections.centralDir),
+                                    DataSources.asDataSource(zipSections.eocd));
+                    if (addV2SignatureRequest != null) {
+                        // Need to insert the returned APK Signing Block before ZIP Central
+                        // Directory.
+                        byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
+                        // Because the APK Signing Block is inserted before the Central Directory,
+                        // we need to adjust accordingly the offset of Central Directory inside the
+                        // ZIP End of Central Directory (EoCD) record.
+                        ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining());
+                        modifiedEocd.put(zipSections.eocd);
+                        modifiedEocd.flip();
+                        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+                        ApkUtils.setZipEocdCentralDirectoryOffset(
+                                modifiedEocd,
+                                zipSections.beforeCentralDir.remaining() + apkSigningBlock.length);
+                        outputChunks =
+                                new ByteBuffer[] {
+                                        zipSections.beforeCentralDir,
+                                        ByteBuffer.wrap(apkSigningBlock),
+                                        zipSections.centralDir,
+                                        modifiedEocd};
+                        addV2SignatureRequest.done();
+                    }
+
+                    // This assumes outputChunks are array-backed. To avoid this assumption, the
+                    // code could be rewritten to use FileChannel.
+                    for (ByteBuffer outputChunk : outputChunks) {
+                        outputFile.write(
+                                outputChunk.array(),
+                                outputChunk.arrayOffset() + outputChunk.position(),
+                                outputChunk.remaining());
+                        outputChunk.position(outputChunk.limit());
+                    }
+
+                    outputFile.close();
+                    outputFile = null;
+                    apkSigner.outputDone();
                 }
 
-                // This assumes outputChunks are array-backed. To avoid this assumption, the
-                // code could be rewritten to use FileChannel.
-                for (ByteBuffer outputChunk : outputChunks) {
-                    outputFile.write(
-                            outputChunk.array(),
-                            outputChunk.arrayOffset() + outputChunk.position(),
-                            outputChunk.remaining());
-                    outputChunk.position(outputChunk.limit());
-                }
-
-                outputFile.close();
-                outputFile = null;
                 return;
             }
         } catch (Exception e) {
diff --git a/tools/signapk/src/com/android/signapk/ZipUtils.java b/tools/signapk/src/com/android/signapk/ZipUtils.java
deleted file mode 100644
index 7575a77..0000000
--- a/tools/signapk/src/com/android/signapk/ZipUtils.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * 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.signapk;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-/**
- * Assorted ZIP format helpers.
- *
- * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
- * order of these buffers is little-endian.
- */
-public abstract class ZipUtils {
-    private ZipUtils() {}
-
-    private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
-    private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
-    private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
-    private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
-    private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
-
-    private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
-    private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
-
-    private static final int UINT16_MAX_VALUE = 0xffff;
-
-    /**
-     * Returns the position at which ZIP End of Central Directory record starts in the provided
-     * buffer or {@code -1} if the record is not present.
-     *
-     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
-     */
-    public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
-        assertByteOrderLittleEndian(zipContents);
-
-        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
-        // The record can be identified by its 4-byte signature/magic which is located at the very
-        // beginning of the record. A complication is that the record is variable-length because of
-        // the comment field.
-        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
-        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
-        // the candidate record's comment length is such that the remainder of the record takes up
-        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
-        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
-
-        int archiveSize = zipContents.capacity();
-        if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
-            return -1;
-        }
-        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
-        int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
-        for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
-                expectedCommentLength++) {
-            int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
-            if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
-                int actualCommentLength =
-                        getUnsignedInt16(
-                                zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
-                if (actualCommentLength == expectedCommentLength) {
-                    return eocdStartPos;
-                }
-            }
-        }
-
-        return -1;
-    }
-
-    /**
-     * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
-     * Locator.
-     *
-     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
-     */
-    public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
-            ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
-        assertByteOrderLittleEndian(zipContents);
-
-        // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
-        // Directory Record.
-
-        int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
-        if (locatorPosition < 0) {
-            return false;
-        }
-
-        return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
-    }
-
-    /**
-     * Returns the offset of the start of the ZIP Central Directory in the archive.
-     *
-     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
-     */
-    public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
-        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
-        return getUnsignedInt32(
-                zipEndOfCentralDirectory,
-                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
-    }
-
-    /**
-     * Sets the offset of the start of the ZIP Central Directory in the archive.
-     *
-     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
-     */
-    public static void setZipEocdCentralDirectoryOffset(
-            ByteBuffer zipEndOfCentralDirectory, long offset) {
-        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
-        setUnsignedInt32(
-                zipEndOfCentralDirectory,
-                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
-                offset);
-    }
-
-    /**
-     * Returns the size (in bytes) of the ZIP Central Directory.
-     *
-     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
-     */
-    public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
-        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
-        return getUnsignedInt32(
-                zipEndOfCentralDirectory,
-                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
-    }
-
-    private 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) {
-        return buffer.getShort(offset) & 0xffff;
-    }
-
-    private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
-        return buffer.getInt(offset) & 0xffffffffL;
-    }
-
-    private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
-        if ((value < 0) || (value > 0xffffffffL)) {
-            throw new IllegalArgumentException("uint32 value of out range: " + value);
-        }
-        buffer.putInt(buffer.position() + offset, (int) value);
-    }
-}
diff --git a/tools/ziptime/Android.bp b/tools/ziptime/Android.bp
new file mode 100644
index 0000000..874d346
--- /dev/null
+++ b/tools/ziptime/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright 2015 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.
+//
+
+//
+// Zip timestamp removal tool
+//
+
+cc_binary_host {
+
+    srcs: [
+        "ZipTime.cpp",
+        "ZipEntry.cpp",
+        "ZipFile.cpp",
+    ],
+
+    name: "ziptime",
+    target: {
+        windows: {
+            enabled: true,
+        },
+    },
+
+}
diff --git a/tools/ziptime/Android.mk b/tools/ziptime/Android.mk
deleted file mode 100644
index 3575229..0000000
--- a/tools/ziptime/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Copyright 2015 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.
-#
-
-#
-# Zip timestamp removal tool
-#
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-	ZipTime.cpp \
-	ZipEntry.cpp \
-	ZipFile.cpp
-
-LOCAL_MODULE := ziptime
-LOCAL_MODULE_HOST_OS := darwin linux windows
-
-include $(BUILD_HOST_EXECUTABLE)