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)