Merge "Fix build system documentation example"
diff --git a/core/clang/arm.mk b/core/clang/arm.mk
index 4053bb2..a5472f4 100644
--- a/core/clang/arm.mk
+++ b/core/clang/arm.mk
@@ -4,12 +4,6 @@
 
 CLANG_CONFIG_arm_EXTRA_CFLAGS :=
 
-ifneq (,$(filter krait,$(TARGET_$(combo_2nd_arch_prefix)CPU_VARIANT)))
-  # Android's clang support's krait as a CPU whereas GCC doesn't. Specify
-  # -mcpu here rather than the more normal core/combo/arch/arm/armv7-a-neon.mk.
-  CLANG_CONFIG_arm_EXTRA_CFLAGS += -mcpu=krait -mfpu=neon-vfpv4
-endif
-
 CLANG_CONFIG_arm_EXTRA_CPPFLAGS :=
 
 CLANG_CONFIG_arm_EXTRA_LDFLAGS :=
@@ -31,6 +25,15 @@
   -fno-tree-copy-prop \
   -fno-tree-loop-optimize
 
+ifneq (,$(filter krait,$(TARGET_$(combo_2nd_arch_prefix)CPU_VARIANT)))
+  # Android's clang support's krait as a CPU whereas GCC doesn't. Specify
+  # -mcpu here rather than the more normal core/combo/arch/arm/armv7-a-neon.mk.
+  CLANG_CONFIG_arm_EXTRA_CFLAGS += -mcpu=krait -mfpu=neon-vfpv4
+
+  # This isn't really unknown, but allows us to only set -mcpu=krait
+  CLANG_CONFIG_arm_UNKNOWN_CFLAGS += -mcpu=cortex-a15
+endif
+
 define subst-clang-incompatible-arm-flags
   $(subst -march=armv5te,-march=armv5t,\
   $(subst -march=armv5e,-march=armv5,\
diff --git a/core/clang/mips.mk b/core/clang/mips.mk
index 4a8f812..aeb2f6a 100644
--- a/core/clang/mips.mk
+++ b/core/clang/mips.mk
@@ -15,11 +15,6 @@
   -mno-synci \
   -mno-fused-madd
 
-# Temporary workaround for Mips clang++ problem,  creates
-#   relocated ptrs in read-only pic .gcc_exception_table;
-#   permanent fix pending at http://reviews.llvm.org/D9669
-CLANG_CONFIG_mips_UNKNOWN_CFLAGS += -Wl,--warn-shared-textrel
-
 # We don't have any mips flags to substitute yet.
 define subst-clang-incompatible-mips-flags
   $(1)
diff --git a/core/clang/mips64.mk b/core/clang/mips64.mk
index 1b72e05..20e87bd 100644
--- a/core/clang/mips64.mk
+++ b/core/clang/mips64.mk
@@ -15,11 +15,6 @@
   -mno-synci \
   -mno-fused-madd
 
-# Temporary workaround for Mips clang++ problem creating
-#   relocated ptrs in read-only pic .gcc_exception_table;
-#   permanent fix pending at http://reviews.llvm.org/D9669
-CLANG_CONFIG_mips64_UNKNOWN_CFLAGS += -Wl,--warn-shared-textrel
-
 # We don't have any mips64 flags to substitute yet.
 define subst-clang-incompatible-mips64-flags
   $(1)
diff --git a/core/combo/TARGET_linux-arm.mk b/core/combo/TARGET_linux-arm.mk
index d89a2cf..9be6c73 100644
--- a/core/combo/TARGET_linux-arm.mk
+++ b/core/combo/TARGET_linux-arm.mk
@@ -119,16 +119,6 @@
 			-fno-strict-volatile-bitfields
 endif
 
-# This is to avoid the dreaded warning compiler message:
-#   note: the mangling of 'va_list' has changed in GCC 4.4
-#
-# The fact that the mangling changed does not affect the NDK ABI
-# very fortunately (since none of the exposed APIs used va_list
-# in their exported C++ functions). Also, GCC 4.5 has already
-# removed the warning from the compiler.
-#
-$(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS += -Wno-psabi
-
 $(combo_2nd_arch_prefix)TARGET_GLOBAL_LDFLAGS += \
 			-Wl,-z,noexecstack \
 			-Wl,-z,relro \
diff --git a/core/combo/TARGET_linux-arm64.mk b/core/combo/TARGET_linux-arm64.mk
index 9ff4981..61028c4 100644
--- a/core/combo/TARGET_linux-arm64.mk
+++ b/core/combo/TARGET_linux-arm64.mk
@@ -95,16 +95,6 @@
 
 TARGET_GLOBAL_CFLAGS += -fno-strict-volatile-bitfields
 
-# This is to avoid the dreaded warning compiler message:
-#   note: the mangling of 'va_list' has changed in GCC 4.4
-#
-# The fact that the mangling changed does not affect the NDK ABI
-# very fortunately (since none of the exposed APIs used va_list
-# in their exported C++ functions). Also, GCC 4.5 has already
-# removed the warning from the compiler.
-#
-TARGET_GLOBAL_CFLAGS += -Wno-psabi
-
 TARGET_GLOBAL_LDFLAGS += \
 			-Wl,-z,noexecstack \
 			-Wl,-z,relro \
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
new file mode 100644
index 0000000..cb0f84a
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+/**
+ * APK Signature Scheme v2 content digest algorithm.
+ */
+enum ContentDigestAlgorithm {
+    /** SHA2-256 over 1 MB chunks. */
+    CHUNKED_SHA256("SHA-256", 256 / 8),
+
+    /** SHA2-512 over 1 MB chunks. */
+    CHUNKED_SHA512("SHA-512", 512 / 8);
+
+    private final String mJcaMessageDigestAlgorithm;
+    private final int mChunkDigestOutputSizeBytes;
+
+    private ContentDigestAlgorithm(
+            String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+        mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
+        mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
+    }
+
+    /**
+     * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
+     * chunks by this content digest algorithm.
+     */
+    String getJcaMessageDigestAlgorithm() {
+        return mJcaMessageDigestAlgorithm;
+    }
+
+    /**
+     * Returns the size (in bytes) of the digest of a chunk of content.
+     */
+    int getChunkDigestOutputSizeBytes() {
+        return mChunkDigestOutputSizeBytes;
+    }
+}
\ No newline at end of file
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/apk/v2/MessageDigestSink.java
new file mode 100644
index 0000000..182b4ed
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
@@ -0,0 +1,52 @@
+/*
+ * 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.util.DataSink;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+/**
+ * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
+ * {@code MessageDigest} instance receives the same data.
+ */
+class MessageDigestSink implements DataSink {
+
+    private final MessageDigest[] mMessageDigests;
+
+    MessageDigestSink(MessageDigest[] digests) {
+        mMessageDigests = digests;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) {
+        for (MessageDigest md : mMessageDigests) {
+            md.update(buf, offset, length);
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) {
+        int originalPosition = buf.position();
+        for (MessageDigest md : mMessageDigests) {
+            // Reset the position back to the original because the previous iteration's
+            // MessageDigest.update set the buffer's position to the buffer's limit.
+            buf.position(originalPosition);
+            md.update(buf);
+        }
+    }
+}
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
new file mode 100644
index 0000000..3c7b5f0
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
@@ -0,0 +1,142 @@
+/*
+ * 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.internal.util.Pair;
+
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+
+/**
+ * APK Signature Scheme v2 content digest algorithm.
+ */
+public enum SignatureAlgorithm {
+    /**
+     * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
+     * digested using SHA2-256 in 1 MB chunks.
+     */
+    RSA_PSS_WITH_SHA256(
+            0x0101,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "RSA",
+            Pair.of("SHA256withRSA/PSS",
+                    new PSSParameterSpec(
+                            "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))),
+
+    /**
+     * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
+     * digested using SHA2-512 in 1 MB chunks.
+     */
+    RSA_PSS_WITH_SHA512(
+            0x0102,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "RSA",
+            Pair.of(
+                    "SHA512withRSA/PSS",
+                    new PSSParameterSpec(
+                            "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))),
+
+    /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    RSA_PKCS1_V1_5_WITH_SHA256(
+            0x0103,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "RSA",
+            Pair.of("SHA256withRSA", null)),
+
+    /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+    RSA_PKCS1_V1_5_WITH_SHA512(
+            0x0104,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "RSA",
+            Pair.of("SHA512withRSA", null)),
+
+    /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    ECDSA_WITH_SHA256(
+            0x0201,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "EC",
+            Pair.of("SHA256withECDSA", null)),
+
+    /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+    ECDSA_WITH_SHA512(
+            0x0202,
+            ContentDigestAlgorithm.CHUNKED_SHA512,
+            "EC",
+            Pair.of("SHA512withECDSA", null)),
+
+    /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+    DSA_WITH_SHA256(
+            0x0301,
+            ContentDigestAlgorithm.CHUNKED_SHA256,
+            "DSA",
+            Pair.of("SHA256withDSA", null));
+
+    private final int mId;
+    private final String mJcaKeyAlgorithm;
+    private final ContentDigestAlgorithm mContentDigestAlgorithm;
+    private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
+
+    private SignatureAlgorithm(int id,
+            ContentDigestAlgorithm contentDigestAlgorithm,
+            String jcaKeyAlgorithm,
+            Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
+        mId = id;
+        mContentDigestAlgorithm = contentDigestAlgorithm;
+        mJcaKeyAlgorithm = jcaKeyAlgorithm;
+        mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
+    }
+
+    /**
+     * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
+     */
+    int getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the content digest algorithm associated with this signature algorithm.
+     */
+    ContentDigestAlgorithm getContentDigestAlgorithm() {
+        return mContentDigestAlgorithm;
+    }
+
+    /**
+     * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
+     */
+    String getJcaKeyAlgorithm() {
+        return mJcaKeyAlgorithm;
+    }
+
+    /**
+     * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
+     * (or null if not needed) to parameterize the {@code Signature}.
+     */
+    Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
+        return mJcaSignatureAlgAndParams;
+    }
+
+    static SignatureAlgorithm findById(int id) {
+        for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+            if (alg.getId() == id) {
+                return alg;
+            }
+        }
+
+        return null;
+    }
+}
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
new file mode 100644
index 0000000..e185346
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
@@ -0,0 +1,614 @@
+/*
+ * 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.internal.util.ByteBufferSink;
+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.util.DataSources;
+
+import java.io.IOException;
+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.interfaces.ECKey;
+import java.security.interfaces.RSAKey;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+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 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.
+ *
+ * <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
+ */
+public abstract class V2SchemeSigner {
+    /*
+     * 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.
+     */
+
+    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;
+
+    /**
+     * Signer configuration.
+     */
+    public static 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.
+         */
+        public List<SignatureAlgorithm> signatureAlgorithms;
+    }
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SchemeSigner() {}
+
+    /**
+     * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
+     * provided key.
+     *
+     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+     *        AndroidManifest.xml minSdkVersion attribute).
+     *
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
+     *         APK Signature Scheme v2
+     */
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
+            PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
+        String keyAlgorithm = signingKey.getAlgorithm();
+        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).
+
+            // Pick a digest which is no weaker than the key.
+            int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
+            if (modulusLengthBits <= 3072) {
+                // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+            } else {
+                // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
+                // digest being the weak link. SHA-512 is the next strongest supported digest.
+                return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+            }
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            // DSA is supported only with SHA-256.
+            return Collections.singletonList(SignatureAlgorithm.DSA_WITH_SHA256);
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            // Pick a digest which is no weaker than the key.
+            int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
+            if (keySizeBits <= 256) {
+                // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
+                return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA256);
+            } else {
+                // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
+                // digest being the weak link. SHA-512 is the next strongest supported digest.
+                return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
+            }
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    /**
+     * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block
+     * containing the signature.
+     *
+     * @param signerConfigs signer configurations, one for each signer At least one signer config
+     *        must be provided.
+     *
+     * @throws IOException if an I/O error occurs
+     * @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 byte[] generateApkSigningBlock(
+            DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd,
+            List<SignerConfig> signerConfigs)
+                        throws IOException, InvalidKeyException, SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "No signer configs provided. At least one is required");
+        }
+
+        // Figure out which digest(s) to use for APK contents.
+        Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1);
+        for (SignerConfig signerConfig : signerConfigs) {
+            for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+                contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
+            }
+        }
+
+        // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
+        // 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);
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
+
+        // Compute digests of APK contents.
+        Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
+        try {
+            contentDigests =
+                    computeContentDigests(
+                            contentDigestAlgorithms,
+                            new DataSource[] {
+                                    beforeCentralDir,
+                                    centralDir,
+                                    DataSources.asDataSource(eocdBuf)});
+        } catch (IOException e) {
+            throw new IOException("Failed to read APK being signed", e);
+        } 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.
+        return generateApkSigningBlock(signerConfigs, contentDigests);
+    }
+
+    private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+            Set<ContentDigestAlgorithm> digestAlgorithms,
+            DataSource[] contents) throws IOException, 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.
+
+        long chunkCountLong = 0;
+        for (DataSource input : contents) {
+            chunkCountLong +=
+                    getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+        }
+        if (chunkCountLong > Integer.MAX_VALUE) {
+            throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+        }
+        int chunkCount = (int) chunkCountLong;
+
+        ContentDigestAlgorithm[] digestAlgorithmsArray =
+                digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
+        MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
+        byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
+        int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
+        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+            int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
+            digestOutputSizes[i] = digestOutputSizeBytes;
+            byte[] concatenationOfChunkCountAndChunkDigests =
+                    new byte[5 + chunkCount * digestOutputSizeBytes];
+            concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
+            setUnsignedInt32LittleEndian(
+                    chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
+            digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
+            String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+            try {
+                mds[i] = MessageDigest.getInstance(jcaAlgorithm);
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(jcaAlgorithm + " MessageDigest not supported", e);
+            }
+        }
+
+        MessageDigestSink mdSink = new MessageDigestSink(mds);
+        byte[] chunkContentPrefix = new byte[5];
+        chunkContentPrefix[0] = (byte) 0xa5;
+        int chunkIndex = 0;
+        // Optimization opportunity: digests of chunks can be computed in parallel. However,
+        // determining the number of computations to be performed in parallel is non-trivial. This
+        // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
+        // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
+        // cores, load on the system from other threads of execution and other processes, size of
+        // input.
+        // For now, we compute these digests sequentially and thus have the luxury of improving
+        // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
+        // the right position. This avoids unnecessary allocations, copying, and enables the final
+        // digest to be more efficient because it's presented with all of its input in one go.
+        for (DataSource input : contents) {
+            long inputOffset = 0;
+            long inputRemaining = input.size();
+            while (inputRemaining > 0) {
+                int chunkSize =
+                        (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+                for (int i = 0; i < mds.length; i++) {
+                    mds[i].update(chunkContentPrefix);
+                }
+                try {
+                    input.feed(inputOffset, chunkSize, mdSink);
+                } catch (IOException e) {
+                    throw new IOException("Failed to read chunk #" + chunkIndex, e);
+                }
+                for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+                    MessageDigest md = mds[i];
+                    byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+                    int expectedDigestSizeBytes = digestOutputSizes[i];
+                    int actualDigestSizeBytes =
+                            md.digest(
+                                    concatenationOfChunkCountAndChunkDigests,
+                                    5 + chunkIndex * expectedDigestSizeBytes,
+                                    expectedDigestSizeBytes);
+                    if (actualDigestSizeBytes != expectedDigestSizeBytes) {
+                        throw new RuntimeException(
+                                "Unexpected output size of " + md.getAlgorithm()
+                                        + " digest: " + actualDigestSizeBytes);
+                    }
+                }
+                inputOffset += chunkSize;
+                inputRemaining -= chunkSize;
+                chunkIndex++;
+            }
+        }
+
+        Map<ContentDigestAlgorithm, byte[]> result = new HashMap<>(digestAlgorithmsArray.length);
+        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+            byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+            MessageDigest md = mds[i];
+            byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
+            result.put(digestAlgorithm, digest);
+        }
+        return result;
+    }
+
+    private static final long getChunkCount(long inputSize, int chunkSize) {
+        return (inputSize + chunkSize - 1) / chunkSize;
+    }
+
+    private static void setUnsignedInt32LittleEndian(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<ContentDigestAlgorithm, 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<ContentDigestAlgorithm, 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<ContentDigestAlgorithm, 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 (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+            ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
+            if (contentDigest == null) {
+                throw new RuntimeException(
+                        contentDigestAlgorithm + " content digest for " + signatureAlgorithm
+                                + " not computed");
+            }
+            digests.add(Pair.of(signatureAlgorithm.getId(), 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<>(signerConfig.signatureAlgorithms.size());
+        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+            Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
+            String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.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.of(signatureAlgorithm.getId(), 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 e) {
+                throw new InvalidKeyException(
+                        "Failed to obtain X.509 encoded form of public key " + publicKey
+                                + " of class " + publicKey.getClass().getName(),
+                        e);
+            } catch (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;
+    }
+
+    private static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
+            throws CertificateEncodingException {
+        List<byte[]> result = new ArrayList<>(certificates.size());
+        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();
+    }
+
+    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/util/ByteBufferDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
new file mode 100644
index 0000000..76f4fda
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link DataSource} backed by a {@link ByteBuffer}.
+ */
+public class ByteBufferDataSource implements DataSource {
+
+    private final ByteBuffer mBuffer;
+    private final long mSize;
+
+    /**
+     * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+     * buffer between the buffer's position and limit.
+     */
+    public ByteBufferDataSource(ByteBuffer buffer) {
+        mBuffer = buffer.slice();
+        mSize = buffer.remaining();
+    }
+
+    @Override
+    public long size() {
+        return mSize;
+    }
+
+    @Override
+    public void feed(long offset, int size, DataSink sink) throws IOException {
+        if (offset < 0) {
+            throw new IllegalArgumentException("offset: " + offset);
+        }
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        if (offset > mSize) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") > source size (" + mSize + ")");
+        }
+        long endOffset = offset + size;
+        if (endOffset < offset) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") + size (" + size + ") overflow");
+        }
+        if (endOffset > mSize) {
+            throw new IllegalArgumentException(
+                    "offset (" + offset + ") + size (" + size + ") > source size (" + mSize  +")");
+        }
+
+        int chunkPosition = (int) offset; // safe to downcast because mSize <= Integer.MAX_VALUE
+        int chunkLimit = (int) endOffset; // safe to downcast because mSize <= Integer.MAX_VALUE
+        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();
+        }
+
+        sink.consume(chunk);
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java
new file mode 100644
index 0000000..8c57905
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.android.apksigner.core.util.DataSink;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Data sink which stores all received data into the associated {@link ByteBuffer}.
+ */
+public class ByteBufferSink implements DataSink {
+
+    private final ByteBuffer mBuffer;
+
+    public ByteBufferSink(ByteBuffer buffer) {
+        mBuffer = buffer;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        try {
+            mBuffer.put(buf, offset, length);
+        } catch (BufferOverflowException e) {
+            throw new IOException(
+                    "Insufficient space in output buffer for " + length + " bytes", e);
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        int length = buf.remaining();
+        try {
+            mBuffer.put(buf);
+        } catch (BufferOverflowException e) {
+            throw new IOException(
+                    "Insufficient space in output buffer for " + length + " bytes", e);
+        }
+    }
+}
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
new file mode 100644
index 0000000..7b47e50
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.zip;
+
+import java.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_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+
+    /**
+     * 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);
+    }
+
+    private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
+        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+        }
+    }
+
+    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);
+    }
+}
\ No newline at end of file
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java
new file mode 100644
index 0000000..35a61fc
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Consumer of input data which may be provided in one go or in chunks.
+ */
+public interface DataSink {
+
+    /**
+     * Consumes the provided chunk of data.
+     *
+     * <p>This data sink guarantees to not hold references to the provided buffer after this method
+     * terminates.
+     */
+    void consume(byte[] buf, int offset, int length) throws IOException;
+
+    /**
+     * Consumes all remaining data in the provided buffer and advances the buffer's position
+     * to the buffer's limit.
+     *
+     * <p>This data sink guarantees to not hold references to the provided buffer after this method
+     * terminates.
+     */
+    void consume(ByteBuffer buf) throws IOException;
+}
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
new file mode 100644
index 0000000..04560cb
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+
+/**
+ * Abstract representation of a source of data.
+ *
+ * <p>This abstraction serves three purposes:
+ * <ul>
+ * <li>Transparent handling of different types of sources, such as {@code byte[]},
+ *     {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.</li>
+ * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
+ *     may have worked as the unifying abstraction.</li>
+ * <li>Support sources which do not fit into logical memory as a contiguous region.</li>
+ * </ul>
+ */
+public interface DataSource {
+
+    /**
+     * Returns the amount of data (in bytes) contained in this data source.
+     */
+    long size();
+
+    /**
+     * Feeds the specified chunk from this data source into the provided sink.
+     *
+     * @param offset index (in bytes) at which the chunk starts inside data source
+     * @param size size (in bytes) of the chunk
+     */
+    void feed(long offset, int size, DataSink sink) throws IOException;
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
new file mode 100644
index 0000000..978afae
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
@@ -0,0 +1,23 @@
+package com.android.apksigner.core.util;
+
+import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for working with {@link DataSource} abstraction.
+ */
+public abstract class DataSources {
+    private DataSources() {}
+
+    /**
+     * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source
+     * represents the data contained between the position and limit of the buffer.
+     */
+    public static DataSource asDataSource(ByteBuffer buffer) {
+        if (buffer == null) {
+            throw new NullPointerException();
+        }
+        return new ByteBufferDataSource(buffer);
+    }
+}