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);
}
}