Merge "Avoid accidentally using the host's native 'as' command."
diff --git a/core/config.mk b/core/config.mk
index 15d8fde..516fec8 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -474,8 +474,10 @@
 MAINDEXCLASSES := $(HOST_OUT_EXECUTABLES)/mainDexClasses
 
 # Always use prebuilts for ckati and makeparallel
-CKATI := $(prebuilt_sdk_tools_bin)/ckati
-MAKEPARALLEL := $(prebuilt_sdk_tools_bin)/makeparallel
+prebuilt_build_tools := prebuilts/build-tools
+prebuilt_build_tools_bin := $(prebuilt_build_tools)/$(HOST_PREBUILT_TAG)/bin
+CKATI := $(prebuilt_build_tools_bin)/ckati
+MAKEPARALLEL := $(prebuilt_build_tools_bin)/makeparallel
 
 USE_PREBUILT_SDK_TOOLS_IN_PLACE := true
 
diff --git a/core/ninja.mk b/core/ninja.mk
index f5d9c89..9e78c46 100644
--- a/core/ninja.mk
+++ b/core/ninja.mk
@@ -1,4 +1,4 @@
-NINJA ?= prebuilts/ninja/$(HOST_PREBUILT_TAG)/ninja
+NINJA ?= prebuilts/build-tools/$(HOST_PREBUILT_TAG)/bin/ninja
 
 include $(BUILD_SYSTEM)/soong.mk
 
@@ -104,13 +104,15 @@
 NINJA_STATUS := [%p %s/%t]$(space)
 endif
 
+NINJA_EXTRA_ARGS :=
+
 ifneq (,$(filter showcommands,$(ORIGINAL_MAKECMDGOALS)))
-NINJA_ARGS += "-v"
+NINJA_EXTRA_ARGS += "-v"
 endif
 
 # Make multiple rules to generate the same target an error instead of
 # proceeding with undefined behavior.
-NINJA_ARGS += -w dupbuild=err
+NINJA_EXTRA_ARGS += -w dupbuild=err
 
 ifdef USE_GOMA
 KATI_MAKEPARALLEL := $(MAKEPARALLEL)
@@ -118,11 +120,13 @@
 # this parallelism. Note the parallelism of all other jobs is still
 # limited by the -j flag passed to GNU make.
 NINJA_REMOTE_NUM_JOBS ?= 500
-NINJA_ARGS += -j$(NINJA_REMOTE_NUM_JOBS)
+NINJA_EXTRA_ARGS += -j$(NINJA_REMOTE_NUM_JOBS)
 else
 NINJA_MAKEPARALLEL := $(MAKEPARALLEL) --ninja
 endif
 
+NINJA_ARGS += $(NINJA_EXTRA_ARGS)
+
 ifeq ($(USE_SOONG),true)
 COMBINED_BUILD_NINJA := $(OUT_DIR)/combined$(KATI_NINJA_SUFFIX).ninja
 
diff --git a/core/soong.mk b/core/soong.mk
index 3450695..990a861 100644
--- a/core/soong.mk
+++ b/core/soong.mk
@@ -79,4 +79,4 @@
 # prebuilts.
 .PHONY: run_soong
 run_soong: $(SOONG_BOOTSTRAP) $(SOONG_VARIABLES) $(SOONG_IN_MAKE) FORCE
-	$(hide) $(SOONG) $(SOONG_BUILD_NINJA) $(NINJA_ARGS)
+	$(hide) $(SOONG) $(SOONG_BUILD_NINJA) $(NINJA_EXTRA_ARGS)
diff --git a/tools/apksigner/Android.mk b/tools/apksigner/Android.mk
new file mode 100644
index 0000000..a7b4414
--- /dev/null
+++ b/tools/apksigner/Android.mk
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/apksigner/core/Android.mk b/tools/apksigner/core/Android.mk
new file mode 100644
index 0000000..c86208b
--- /dev/null
+++ b/tools/apksigner/core/Android.mk
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+# apksigner library, for signing APKs and verification signatures of APKs
+# ============================================================
+include $(CLEAR_VARS)
+LOCAL_MODULE := apksigner-core
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_JAVA_LIBRARIES = \
+  bouncycastle-host \
+  bouncycastle-bcpkix-host
+include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java
new file mode 100644
index 0000000..71e698b
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v1;
+
+/**
+ * Digest algorithm used with JAR signing (aka v1 signing scheme).
+ */
+public enum DigestAlgorithm {
+    /** SHA-1 */
+    SHA1("SHA-1"),
+
+    /** SHA2-256 */
+    SHA256("SHA-256");
+
+    private final String mJcaMessageDigestAlgorithm;
+
+    private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
+        mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
+    }
+
+    /**
+     * Returns the {@link java.security.MessageDigest} algorithm represented by this digest
+     * algorithm.
+     */
+    String getJcaMessageDigestAlgorithm() {
+        return mJcaMessageDigestAlgorithm;
+    }
+}
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
new file mode 100644
index 0000000..b99cdec
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v1;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.DEROutputStream;
+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.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+import com.android.apksigner.core.internal.jar.ManifestWriter;
+import com.android.apksigner.core.internal.jar.SignatureFileWriter;
+import com.android.apksigner.core.internal.util.Pair;
+
+/**
+ * APK signer which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeSigner {
+
+    public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+
+    private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
+            new Attributes.Name("Created-By");
+    private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)";
+    private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
+    private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
+
+    private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
+            new Attributes.Name("X-Android-APK-Signed");
+
+    /**
+     * Signer configuration.
+     */
+    public static class SignerConfig {
+        /** Name. */
+        public String name;
+
+        /** Private key. */
+        public PrivateKey privateKey;
+
+        /**
+         * Certificates, with the first certificate containing the public key corresponding to
+         * {@link #privateKey}.
+         */
+        public List<X509Certificate> certificates;
+
+        /**
+         * Digest algorithm used for the signature.
+         */
+        public DigestAlgorithm signatureDigestAlgorithm;
+
+        /**
+         * Digest algorithm used for digests of JAR entries and MANIFEST.MF.
+         */
+        public DigestAlgorithm contentDigestAlgorithm;
+    }
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SchemeSigner() {}
+
+    /**
+     * Gets the JAR signing digest algorithm 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
+     *         JAR signing (aka v1 signature scheme)
+     */
+    public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
+            PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
+        String keyAlgorithm = signingKey.getAlgorithm();
+        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+            // Prior to API Level 18, only SHA-1 can be used with RSA.
+            if (minSdkVersion < 18) {
+                return DigestAlgorithm.SHA1;
+            }
+            return DigestAlgorithm.SHA256;
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            // Prior to API Level 21, only SHA-1 can be used with DSA
+            if (minSdkVersion < 21) {
+                return DigestAlgorithm.SHA1;
+            } else {
+                return DigestAlgorithm.SHA256;
+            }
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            if (minSdkVersion < 18) {
+                throw new InvalidKeyException(
+                        "ECDSA signatures only supported for minSdkVersion 18 and higher");
+            }
+            // Prior to API Level 21, only SHA-1 can be used with ECDSA
+            if (minSdkVersion < 21) {
+                return DigestAlgorithm.SHA1;
+            } else {
+                return DigestAlgorithm.SHA256;
+            }
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    /**
+     * Returns the JAR signing digest algorithm to be used for JAR entry digests.
+     *
+     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+     *        AndroidManifest.xml minSdkVersion attribute)
+     */
+    public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) {
+        return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1;
+    }
+
+    /**
+     * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
+     */
+    public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) {
+        String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+        try {
+            return MessageDigest.getInstance(jcaAlgorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+     * manifest.
+     */
+    public static boolean isJarEntryDigestNeededInManifest(String entryName) {
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
+
+        // Entries outside of META-INF must be listed in the manifest.
+        if (!entryName.startsWith("META-INF/")) {
+            return true;
+        }
+        // Entries in subdirectories of META-INF must be listed in the manifest.
+        if (entryName.indexOf('/', "META-INF/".length()) != -1) {
+            return true;
+        }
+
+        // Ignored file names (case-insensitive) in META-INF directory:
+        //   MANIFEST.MF
+        //   *.SF
+        //   *.RSA
+        //   *.DSA
+        //   *.EC
+        //   SIG-*
+        String fileNameLowerCase =
+                entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
+        if (("manifest.mf".equals(fileNameLowerCase))
+                || (fileNameLowerCase.endsWith(".sf"))
+                || (fileNameLowerCase.endsWith(".rsa"))
+                || (fileNameLowerCase.endsWith(".dsa"))
+                || (fileNameLowerCase.endsWith(".ec"))
+                || (fileNameLowerCase.startsWith("sig-"))) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+     * JAR entries which need to be added to the APK as part of the signature.
+     *
+     * @param signerConfigs signer configurations, one for each signer. At least one signer config
+     *        must be provided.
+     *
+     * @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 List<Pair<String, byte[]>> sign(
+            List<SignerConfig> signerConfigs,
+            DigestAlgorithm jarEntryDigestAlgorithm,
+            Map<String, byte[]> jarEntryDigests,
+            List<Integer> apkSigningSchemeIds,
+            byte[] sourceManifestBytes)
+                    throws InvalidKeyException, CertificateEncodingException, SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException("At least one signer config must be provided");
+        }
+        OutputManifestFile manifest =
+                generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
+
+        return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest);
+    }
+
+    /**
+     * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+     * JAR entries which need to be added to the APK as part of the signature.
+     *
+     * @param signerConfigs signer configurations, one for each signer. At least one signer config
+     *        must be provided.
+     *
+     * @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 List<Pair<String, byte[]>> signManifest(
+            List<SignerConfig> signerConfigs,
+            DigestAlgorithm digestAlgorithm,
+            List<Integer> apkSigningSchemeIds,
+            OutputManifestFile manifest)
+                    throws InvalidKeyException, CertificateEncodingException, SignatureException {
+        if (signerConfigs.isEmpty()) {
+            throw new IllegalArgumentException("At least one signer config must be provided");
+        }
+
+        // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
+        List<Pair<String, byte[]>> signatureJarEntries =
+                new ArrayList<>(2 * signerConfigs.size() + 1);
+        byte[] sfBytes =
+                generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest);
+        for (SignerConfig signerConfig : signerConfigs) {
+            String signerName = signerConfig.name;
+            byte[] signatureBlock;
+            try {
+                signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
+            } catch (InvalidKeyException e) {
+                throw new InvalidKeyException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            } catch (CertificateEncodingException e) {
+                throw new CertificateEncodingException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            } catch (SignatureException e) {
+                throw new SignatureException(
+                        "Failed to sign using signer \"" + signerName + "\"", e);
+            }
+            signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
+            PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+            String signatureBlockFileName =
+                    "META-INF/" + signerName + "."
+                            + publicKey.getAlgorithm().toUpperCase(Locale.US);
+            signatureJarEntries.add(
+                    Pair.of(signatureBlockFileName, signatureBlock));
+        }
+        signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
+        return signatureJarEntries;
+    }
+
+    /**
+     * Returns the names of JAR entries which this signer will produce as part of v1 signature.
+     */
+    public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
+        Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
+        for (SignerConfig signerConfig : signerConfigs) {
+            String signerName = signerConfig.name;
+            result.add("META-INF/" + signerName + ".SF");
+            PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+            String signatureBlockFileName =
+                    "META-INF/" + signerName + "."
+                            + publicKey.getAlgorithm().toUpperCase(Locale.US);
+            result.add(signatureBlockFileName);
+        }
+        result.add(MANIFEST_ENTRY_NAME);
+        return result;
+    }
+
+    /**
+     * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
+     * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
+     */
+    public static OutputManifestFile generateManifestFile(
+            DigestAlgorithm jarEntryDigestAlgorithm,
+            Map<String, byte[]> jarEntryDigests,
+            byte[] sourceManifestBytes) {
+        Manifest sourceManifest = null;
+        if (sourceManifestBytes != null) {
+            try {
+                sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
+            } catch (IOException e) {
+                throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e);
+            }
+        }
+        ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
+        Attributes mainAttrs = new Attributes();
+        // Copy the main section from the source manifest (if provided). Otherwise use defaults.
+        if (sourceManifest != null) {
+            mainAttrs.putAll(sourceManifest.getMainAttributes());
+        } else {
+            mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
+            mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
+        }
+
+        try {
+            ManifestWriter.writeMainSection(manifestOut, mainAttrs);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+        }
+
+        List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
+        Collections.sort(sortedEntryNames);
+        SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
+        String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
+        for (String entryName : sortedEntryNames) {
+            byte[] entryDigest = jarEntryDigests.get(entryName);
+            Attributes entryAttrs = new Attributes();
+            entryAttrs.putValue(
+                    entryDigestAttributeName,
+                    Base64.getEncoder().encodeToString(entryDigest));
+            ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
+            byte[] sectionBytes;
+            try {
+                ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
+                sectionBytes = sectionOut.toByteArray();
+                manifestOut.write(sectionBytes);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+            }
+            invidualSectionsContents.put(entryName, sectionBytes);
+        }
+
+        OutputManifestFile result = new OutputManifestFile();
+        result.contents = manifestOut.toByteArray();
+        result.mainSectionAttributes = mainAttrs;
+        result.individualSectionsContents = invidualSectionsContents;
+        return result;
+    }
+
+    public static class OutputManifestFile {
+        public byte[] contents;
+        public SortedMap<String, byte[]> individualSectionsContents;
+        public Attributes mainSectionAttributes;
+    }
+
+    private static byte[] generateSignatureFile(
+            List<Integer> apkSignatureSchemeIds,
+            DigestAlgorithm manifestDigestAlgorithm,
+            OutputManifestFile manifest) {
+        Manifest sf = new Manifest();
+        Attributes mainAttrs = sf.getMainAttributes();
+        mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
+        mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
+        if (!apkSignatureSchemeIds.isEmpty()) {
+            // Add APK Signature Scheme v2 (and newer) 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.
+            StringBuilder attrValue = new StringBuilder();
+            for (int id : apkSignatureSchemeIds) {
+                if (attrValue.length() > 0) {
+                    attrValue.append(", ");
+                }
+                attrValue.append(String.valueOf(id));
+            }
+            mainAttrs.put(
+                    SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
+                    attrValue.toString());
+        }
+
+        // Add main attribute containing the digest of MANIFEST.MF.
+        MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
+        mainAttrs.putValue(
+                getManifestDigestAttributeName(manifestDigestAlgorithm),
+                Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            SignatureFileWriter.writeMainSection(out, mainAttrs);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to write in-memory .SF file", e);
+        }
+        String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
+        for (Map.Entry<String, byte[]> manifestSection
+                : manifest.individualSectionsContents.entrySet()) {
+            String sectionName = manifestSection.getKey();
+            byte[] sectionContents = manifestSection.getValue();
+            byte[] sectionDigest = md.digest(sectionContents);
+            Attributes attrs = new Attributes();
+            attrs.putValue(
+                    entryDigestAttributeName,
+                    Base64.getEncoder().encodeToString(sectionDigest));
+
+            try {
+                SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write in-memory .SF file", e);
+            }
+        }
+
+        // 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 ((out.size() > 0) && ((out.size() % 1024) == 0)) {
+            try {
+                SignatureFileWriter.writeSectionDelimiter(out);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
+            }
+        }
+
+        return out.toByteArray();
+    }
+
+    private static byte[] generateSignatureBlock(
+            SignerConfig signerConfig, byte[] signatureFileBytes)
+                    throws InvalidKeyException, CertificateEncodingException, SignatureException {
+        JcaCertStore certs = new JcaCertStore(signerConfig.certificates);
+        X509Certificate signerCert = signerConfig.certificates.get(0);
+        String jcaSignatureAlgorithm =
+                getJcaSignatureAlgorithm(
+                        signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm);
+        try {
+            ContentSigner signer =
+                    new JcaContentSignerBuilder(jcaSignatureAlgorithm)
+                    .build(signerConfig.privateKey);
+            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+            gen.addSignerInfoGenerator(
+                    new JcaSignerInfoGeneratorBuilder(
+                            new JcaDigestCalculatorProviderBuilder().build())
+                    .setDirectSignature(true)
+                    .build(signer, signerCert));
+            gen.addCertificates(certs);
+
+            CMSSignedData sigData =
+                    gen.generate(new CMSProcessableByteArray(signatureFileBytes), false);
+
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
+                DEROutputStream dos = new DEROutputStream(out);
+                dos.writeObject(asn1.readObject());
+            }
+            return out.toByteArray();
+        } catch (OperatorCreationException | CMSException | IOException e) {
+            throw new SignatureException("Failed to generate signature", e);
+        }
+    }
+
+    private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return "SHA1-Digest";
+            case SHA256:
+                return "SHA-256-Digest";
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected content digest algorithm: " + digestAlgorithm);
+        }
+    }
+
+    private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return "SHA1-Digest-Manifest";
+            case SHA256:
+                return "SHA-256-Digest-Manifest";
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected content digest algorithm: " + digestAlgorithm);
+        }
+    }
+
+    private static String getJcaSignatureAlgorithm(
+            PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+        String keyAlgorithm = publicKey.getAlgorithm();
+        String digestPrefixForSigAlg;
+        switch (digestAlgorithm) {
+            case SHA1:
+                digestPrefixForSigAlg = "SHA1";
+                break;
+            case SHA256:
+                digestPrefixForSigAlg = "SHA256";
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected digest algorithm: " + digestAlgorithm);
+        }
+        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+            return digestPrefixForSigAlg + "withRSA";
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            return digestPrefixForSigAlg + "withDSA";
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            return digestPrefixForSigAlg + "withECDSA";
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java
new file mode 100644
index 0000000..449953a
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.jar;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+
+/**
+ * Producer of {@code META-INF/MANIFEST.MF} file.
+ */
+public abstract class ManifestWriter {
+
+    private static final byte[] CRLF = new byte[] {'\r', '\n'};
+    private static final int MAX_LINE_LENGTH = 70;
+
+    private ManifestWriter() {}
+
+    public static void writeMainSection(OutputStream out, Attributes attributes)
+            throws IOException {
+
+        // Main section must start with the Manifest-Version attribute.
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+        String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
+        if (manifestVersion == null) {
+            throw new IllegalArgumentException(
+                    "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
+        }
+        writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
+
+        if (attributes.size() > 1) {
+            SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
+            namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
+            writeAttributes(out, namedAttributes);
+        }
+        writeSectionDelimiter(out);
+    }
+
+    public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+            throws IOException {
+        writeAttribute(out, "Name", name);
+
+        if (!attributes.isEmpty()) {
+            writeAttributes(out, getAttributesSortedByName(attributes));
+        }
+        writeSectionDelimiter(out);
+    }
+
+    static void writeSectionDelimiter(OutputStream out) throws IOException {
+        out.write(CRLF);
+    }
+
+    static void writeAttribute(OutputStream  out, Attributes.Name name, String value)
+            throws IOException {
+        writeAttribute(out, name.toString(), value);
+    }
+
+    private static void writeAttribute(OutputStream  out, String name, String value)
+            throws IOException {
+        writeLine(out, name + ": " + value);
+    }
+
+    private static void writeLine(OutputStream  out, String line) throws IOException {
+        byte[] lineBytes = line.getBytes("UTF-8");
+        int offset = 0;
+        int remaining = lineBytes.length;
+        boolean firstLine = true;
+        while (remaining > 0) {
+            int chunkLength;
+            if (firstLine) {
+                // First line
+                chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
+            } else {
+                // Continuation line
+                out.write(CRLF);
+                out.write(' ');
+                chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
+            }
+            out.write(lineBytes, offset, chunkLength);
+            offset += chunkLength;
+            remaining -= chunkLength;
+            firstLine = false;
+        }
+        out.write(CRLF);
+    }
+
+    static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
+        Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
+        SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
+        for (Map.Entry<Object, Object> attribute : attributesEntries) {
+            String attrName = attribute.getKey().toString();
+            String attrValue = attribute.getValue().toString();
+            namedAttributes.put(attrName, attrValue);
+        }
+        return namedAttributes;
+    }
+
+    static void writeAttributes(
+            OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
+        for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
+            String attrName = attribute.getKey();
+            String attrValue = attribute.getValue();
+            writeAttribute(out, attrName, attrValue);
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java
new file mode 100644
index 0000000..9cd25f3
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.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.jar;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.SortedMap;
+import java.util.jar.Attributes;
+
+/**
+ * Producer of JAR signature file ({@code *.SF}).
+ */
+public abstract class SignatureFileWriter {
+    private SignatureFileWriter() {}
+
+    public static void writeMainSection(OutputStream out, Attributes attributes)
+            throws IOException {
+
+        // Main section must start with the Signature-Version attribute.
+        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+        String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
+        if (signatureVersion == null) {
+            throw new IllegalArgumentException(
+                    "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
+        }
+        ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
+
+        if (attributes.size() > 1) {
+            SortedMap<String, String> namedAttributes =
+                    ManifestWriter.getAttributesSortedByName(attributes);
+            namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
+            ManifestWriter.writeAttributes(out, namedAttributes);
+        }
+        writeSectionDelimiter(out);
+    }
+
+    public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+            throws IOException {
+        ManifestWriter.writeIndividualSection(out, name, attributes);
+    }
+
+    public static void writeSectionDelimiter(OutputStream out) throws IOException {
+        ManifestWriter.writeSectionDelimiter(out);
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java
new file mode 100644
index 0000000..d59af41
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+/**
+ * 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> of(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 69f17e2..c80d93c 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -82,6 +82,7 @@
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
 import java.util.regex.Pattern;
+
 import javax.crypto.Cipher;
 import javax.crypto.EncryptedPrivateKeyInfo;
 import javax.crypto.SecretKeyFactory;
@@ -126,34 +127,29 @@
     private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
 
     /**
-     * Minimum Android SDK API Level which accepts JAR signatures which use SHA-256. Older platform
-     * versions accept only SHA-1 signatures.
-     */
-    private static final int MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES = 18;
-
-    /**
      * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
-     * for v1 signing (using JAR Signature Scheme) an APK using the private key corresponding to the
-     * provided certificate.
+     * 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 sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
-        if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
-            // see "HISTORICAL NOTE" above.
-            if (minSdkVersion < MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES) {
-                return USE_SHA1;
-            } else {
-                return USE_SHA256;
+        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");
             }
-        } else if (sigAlg.startsWith("SHA256WITH")) {
-            return USE_SHA256;
+            return (minSdkVersion < 21) ? USE_SHA1 : USE_SHA256;
         } else {
-            throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
-                                               "\" in cert [" + cert.getSubjectDN());
+            throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
         }
     }