summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Klyubin <klyubin@google.com>2018-01-25 23:00:30 +0000
committerandroid-build-merger <android-build-merger@google.com>2018-01-25 23:00:30 +0000
commitf0708f1f3a0bd235c092755db1454a259529e727 (patch)
tree57f0f46a6ee4bc526c2cd1bf77f3eeeaa5441d3c
parentcfc0b319a58800a0211f306da1d90ff901fe331d (diff)
parent4b9d059cfed959f3578c34d8810b88ee393309ea (diff)
downloadapkzlib-f0708f1f3a0bd235c092755db1454a259529e727.tar.gz
Use apksig's ApkSignerEngine for signing APKs am: a96ce3418b
am: 4b9d059cfe Change-Id: Ib33785c2cc328c23a14cfb6ebcaeaf15bac1f2dd
-rw-r--r--BUILD1
-rw-r--r--apkzlib.iml1
-rw-r--r--build.gradle1
-rw-r--r--src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java29
-rw-r--r--src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java211
-rw-r--r--src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java106
-rw-r--r--src/main/java/com/android/apkzlib/sign/SignatureExtension.java630
-rw-r--r--src/main/java/com/android/apkzlib/sign/SigningExtension.java392
-rw-r--r--src/main/java/com/android/apkzlib/sign/ZFileDataSource.java157
-rw-r--r--src/main/java/com/android/apkzlib/sign/package-info.java33
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java596
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java59
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java52
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/DigestSource.java50
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java114
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java68
-rw-r--r--src/main/java/com/android/apkzlib/zfile/ZFiles.java39
-rw-r--r--src/main/java/com/android/apkzlib/zip/StoredEntry.java7
-rw-r--r--src/main/java/com/android/apkzlib/zip/ZFile.java85
-rw-r--r--src/test/java/com/android/apkzlib/sign/FullApkSignTest.java7
-rw-r--r--src/test/java/com/android/apkzlib/sign/JarSigningTest.java26
21 files changed, 674 insertions, 1990 deletions
diff --git a/BUILD b/BUILD
index 9cb0fde..bebbfbd 100644
--- a/BUILD
+++ b/BUILD
@@ -16,6 +16,7 @@ java_library(
]),
visibility = ["//visibility:public"],
deps = [
+ "//tools/base/build-system:tools.apksig",
"//tools/base/third_party:com.google.code.findbugs_jsr305",
"//tools/base/third_party:com.google.guava_guava",
"//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on",
diff --git a/apkzlib.iml b/apkzlib.iml
index dce8c3c..70ffee4 100644
--- a/apkzlib.iml
+++ b/apkzlib.iml
@@ -15,5 +15,6 @@
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="library" name="bouncy-castle" level="project" />
<orderEntry type="module" module-name="testutils" />
+ <orderEntry type="module" module-name="apksig" />
</component>
</module>
diff --git a/build.gradle b/build.gradle
index aa89c1c..32ff351 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,6 +5,7 @@ dependencies {
compile 'com.google.guava:guava:18.0'
compile 'org.bouncycastle:bcpkix-jdk15on:1.48'
compile 'org.bouncycastle:bcprov-jdk15on:1.48'
+ compile project(':apksig')
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-all:1.9.5'
diff --git a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
index 6833d2f..64427ae 100644
--- a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
@@ -40,14 +40,15 @@ public enum DigestAlgorithm {
SHA256("SHA-256", "SHA-256");
/**
- * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA}.
+ * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and
+ * {@link SignatureAlgorithm#ECDSA}.
*/
- public static final int API_SHA_256_RSA = 18;
+ public static final int API_SHA_256_RSA_AND_ECDSA = 18;
/**
* API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.
*
- * <p>Before that, SHA256 can only be used with RSA.
+ * <p>Before that, SHA256 can only be used with RSA and ECDSA.
*/
public static final int API_SHA_256_ALL_ALGORITHMS = 21;
@@ -80,26 +81,4 @@ public enum DigestAlgorithm {
this.entryAttributeName = attributeName + "-Digest";
this.manifestAttributeName = attributeName + "-Digest-Manifest";
}
-
- /**
- * Finds the best digest algorithm applicable for a given SDK.
- *
- * @param minSdk the minimum SDK
- * @param signatureAlgorithm signature algorithm used
- * @return the best algorithm found
- */
- @Nonnull
- public static DigestAlgorithm findBest(
- int minSdk,
- @Nonnull SignatureAlgorithm signatureAlgorithm) {
- if (signatureAlgorithm == SignatureAlgorithm.RSA) {
- // PKCS #7 RSA signatures with SHA-256 are
- // supported only since API Level 18 (JB MR2).
- return minSdk >= API_SHA_256_RSA ? SHA256 : SHA1;
- } else {
- // PKCS #7 ECDSA and DSA signatures with SHA-256
- // are supported only since API Level 21 (Android L).
- return minSdk >= API_SHA_256_ALL_ALGORITHMS ? SHA256 : SHA1;
- }
- }
}
diff --git a/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java b/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java
deleted file mode 100644
index ef5f547..0000000
--- a/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign;
-
-import com.android.apkzlib.sign.v2.ApkSignerV2;
-import com.android.apkzlib.sign.v2.ByteArrayDigestSource;
-import com.android.apkzlib.sign.v2.DigestSource;
-import com.android.apkzlib.sign.v2.ZFileDigestSource;
-import com.android.apkzlib.utils.IOExceptionRunnable;
-import com.android.apkzlib.zip.StoredEntry;
-import com.android.apkzlib.zip.ZFile;
-import com.android.apkzlib.zip.ZFileExtension;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.PrivateKey;
-import java.security.SignatureException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-/**
- * Extension that adds full APK signing. This extension will:
- * <ul>
- * <li>Generate a new signature if the zip is modified after the extension is added.</li>
- * <li>Generate a new signature even if the zip is not modified after the extension is
- * added, but a valid signature is not found.</li>
- * </ul>
- * <p>
- * The extension computes the signature, if needed, at the {@link ZFileExtension#entriesWritten()}
- * event, after all zip entries have been written, but before the central directory or EOCD have.
- * This allows the extension to set the central directory offset parameter in the zip file
- * using {@link ZFile#setExtraDirectoryOffset(long)} adding enough space in the zip file for the
- * signature block.
- * <p>
- * The signature block is written before the central directory, allowing the zip file to be written
- * in sequential order.
- */
-public class FullApkSignExtension {
-
- /**
- * The zip file this extension is registered with.
- */
- @Nonnull
- private final ZFile mFile;
-
- /**
- * Signer certificate.
- */
- @Nonnull
- private final X509Certificate mCertificate;
-
- /**
- * Signer private key.
- */
- @Nonnull
- private final PrivateKey mPrivateKey;
-
- /**
- * APK Signature Scheme v2 algorithms to use for signing the APK.
- */
- private final List<com.android.apkzlib.sign.v2.SignatureAlgorithm> mV2SignatureAlgorithms;
-
- /**
- * {@code true} if the zip needs its signature to be updated.
- */
- private boolean mNeedsSignatureUpdate = true;
-
- /**
- * The extension to register with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new extension. This will not register the extension with the provided
- * {@link ZFile}. Until {@link #register()} is invoked, this extension is not used.
- *
- * @param file the zip file to register the extension with
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- *
- * @throws InvalidKeyException if the signing key is not suitable for signing this APK.
- */
- public FullApkSignExtension(@Nonnull ZFile file,
- int minSdkVersion,
- @Nonnull X509Certificate certificate,
- @Nonnull PrivateKey privateKey) throws InvalidKeyException {
- mFile = file;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mV2SignatureAlgorithms =
- ApkSignerV2.getSuggestedSignatureAlgorithms(
- certificate.getPublicKey(), minSdkVersion);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the constructor.
- */
- public void register() {
- Preconditions.checkState(mExtension == null, "register() has already been invoked.");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() throws IOException {
- mFile.setExtraDirectoryOffset(0);
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@Nonnull StoredEntry entry,
- @Nullable StoredEntry replaced) {
- onZipChanged();
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
- onZipChanged();
- return null;
- }
-
- @Override
- public void entriesWritten() throws IOException {
- onEntriesWritten();
- }
- };
-
- mFile.addZFileExtension(mExtension);
- }
-
- /**
- * Invoked when the zip file has been changed.
- */
- private void onZipChanged() {
- mNeedsSignatureUpdate = true;
- }
-
- /**
- * Invoked before the zip file has been updated.
- *
- * @throws IOException failed to perform the update
- */
- private void onEntriesWritten() throws IOException {
- if (!mNeedsSignatureUpdate) {
- return;
- }
- mNeedsSignatureUpdate = false;
-
- byte[] apkSigningBlock = generateApkSigningBlock();
- Verify.verify(apkSigningBlock.length > 0, "apkSigningBlock.length == 0");
- mFile.setExtraDirectoryOffset(apkSigningBlock.length);
- long apkSigningBlockOffset =
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset();
- mFile.directWrite(apkSigningBlockOffset, apkSigningBlock);
- }
-
- /**
- * Generates a signature for the APK.
- *
- * @return the signature data block
- * @throws IOException failed to generate a signature
- */
- @Nonnull
- private byte[] generateApkSigningBlock() throws IOException {
- byte[] centralDirectoryData = mFile.getCentralDirectoryBytes();
- byte[] eocdData = mFile.getEocdBytes();
-
- ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
- signerConfig.privateKey = mPrivateKey;
- signerConfig.certificates = ImmutableList.of(mCertificate);
- signerConfig.signatureAlgorithms = mV2SignatureAlgorithms;
- DigestSource centralDir = new ByteArrayDigestSource(centralDirectoryData);
- DigestSource eocd = new ByteArrayDigestSource(eocdData);
- DigestSource zipEntries =
- new ZFileDigestSource(
- mFile,
- 0,
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset());
- try {
- return ApkSignerV2.generateApkSigningBlock(
- zipEntries,
- centralDir,
- eocd,
- ImmutableList.of(signerConfig));
- } catch (InvalidKeyException | SignatureException e) {
- throw new IOException("Failed to sign APK using APK Signature Scheme v2", e);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
index 02e8f06..a8f9b98 100644
--- a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
+++ b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
@@ -24,12 +24,10 @@ import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
-import com.google.common.collect.Maps;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.annotation.Nonnull;
@@ -56,12 +54,12 @@ public class ManifestGenerationExtension {
/**
* Name of META-INF directory.
*/
- public static final String META_INF_DIR = "META-INF";
+ private static final String META_INF_DIR = "META-INF";
/**
* Name of the manifest file.
*/
- public static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
+ static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
/**
* Who should be reported as the manifest builder.
@@ -243,104 +241,4 @@ public class ManifestGenerationExtension {
mZFile.add(MANIFEST_NAME, new ByteArrayInputStream(mManifestBytes.get()));
mDirty = false;
}
-
- /**
- * Obtains the {@link ZFile} this extension is associated with. This method can only be invoked
- * after {@link #register(ZFile)} has been invoked.
- *
- * @return the {@link ZFile}
- */
- @Nonnull
- public ZFile zFile() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile;
- }
-
- /**
- * Obtains the stored entry in the {@link ZFile} that contains the manifest. This method can
- * only be invoked after {@link #register(ZFile)} has been invoked.
- *
- * @return the entry, {@code null} if none
- */
- @Nullable
- public StoredEntry manifestEntry() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile.get(MANIFEST_NAME);
- }
-
- /**
- * Obtains an attribute of an entry.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @return the attribute value or {@code null} if the entry does not have any attributes or
- * if it doesn't have the specified attribute
- */
- @Nullable
- public String getAttribute(@Nonnull String entryName, @Nonnull String attr) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- return null;
- }
-
- return attrs.getValue(attr);
- }
-
- /**
- * Sets the value of an attribute of an entry. If this entry's attribute already has the given
- * value, this method does nothing.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @param value the attribute value
- */
- public void setAttribute(@Nonnull String entryName, @Nonnull String attr,
- @Nonnull String value) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- attrs = new Attributes();
- markDirty();
- mManifest.getEntries().put(entryName, attrs);
- }
-
- String current = attrs.getValue(attr);
- if (!value.equals(current)) {
- attrs.putValue(attr, value);
- markDirty();
- }
- }
-
- /**
- * Obtains the current manifest.
- *
- * @return a byte sequence representation of the manifest that is guaranteed not to change if
- * the manifest is not modified
- * @throws IOException failed to compute the manifest's byte representation
- */
- @Nonnull
- public byte[] getManifestBytes() throws IOException {
- return mManifestBytes.get();
- }
-
- /**
- * Obtains all entries and all attributes they have in the manifest.
- *
- * @return a map that relates entry names to entry attributes
- */
- @Nonnull
- public Map<String, Attributes> allEntries() {
- return Maps.newHashMap(mManifest.getEntries());
- }
-
- /**
- * Removes an entry from the manifest. If no entry exists with the given name, this operation
- * does nothing.
- *
- * @param name the entry's name
- */
- public void removeEntry(@Nonnull String name) {
- if (mManifest.getEntries().remove(name) != null) {
- markDirty();
- }
- }
}
diff --git a/src/main/java/com/android/apkzlib/sign/SignatureExtension.java b/src/main/java/com/android/apkzlib/sign/SignatureExtension.java
deleted file mode 100644
index 49049b6..0000000
--- a/src/main/java/com/android/apkzlib/sign/SignatureExtension.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign;
-
-import com.android.apkzlib.utils.IOExceptionRunnable;
-import com.android.apkzlib.zip.StoredEntry;
-import com.android.apkzlib.zip.ZFile;
-import com.android.apkzlib.zip.ZFileExtension;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Locale;
-import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-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;
-
-/**
- * {@link ZFile} extension that signs all files in the APK and generates a signature file and a
- * digital signature of the signature file. The extension registers itself automatically with the
- * {@link ZFile} upon creation.
- * <p>
- * The signature extension will recompute signatures of files already in the zip file but won't
- * update the manifest if these signatures match the ones in the manifest.
- * <p>
- * This extension does 4 main tasks: maintaining the digests of all files in the zip in the manifest
- * file, maintaining the digests of all files in the zip in the signature file, maintaining the
- * digest of the manifest in the signature file and maintaining the digital signature file. For
- * performance, the digests and signatures are only computed when needed.
- * <p>
- * These tasks are done at three different moments: when the extension
- * is created, when files are added to the zip and before the zip is updated.
- * When the extension is created: (Note that the manifest's digest is <em>not</em> checked when
- * the extension is created.)
- * <ul>
- * <li>The signature file is read, if one exists.
- * <li>The signature "administrative" info is read and updated if not up-to-date.
- * <li>The digests for entries in the manifest and signature file that do not correspond to
- * any file in the zip are removed.
- * <li>The digests for all entries in the zip are recomputed and updated in the signature file
- * and in the manifest, if needed.
- * </ul>
- * <p>
- * When files are added or removed:
- * <ul>
- * <li>The signature file and manifest are updated to reflect the changes.
- * <li>If the file was added, its digest is computed.
- * </ul>
- * <p>
- * Before updating the zip file:
- * <ul>
- * <li>If a signature file already exists, checks the digest of the manifest and updates the
- * signature file if needed.
- * <li>Creates the signature file if it did not already exist.
- * <li>Recreates the digital signature of the signature file if the signature file was created
- * or updated.
- * </ul>
- */
-public class SignatureExtension {
-
- /**
- * Base of signature files.
- */
- private static final String SIGNATURE_BASE = ManifestGenerationExtension.META_INF_DIR + "/CERT";
-
- /**
- * Path of the signature file.
- */
- private static final String SIGNATURE_FILE = SIGNATURE_BASE + ".SF";
-
- /**
- * Name of attribute with the signature version.
- */
- private static final String SIGNATURE_VERSION_NAME = "Signature-Version";
-
- /**
- * Version of the signature version.
- */
- private static final String SIGNATURE_VERSION_VALUE = "1.0";
-
- /**
- * Name of attribute with the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_NAME = "Created-By";
-
- /**
- * Value of the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_VALUE = "1.0 (Android)";
-
- /**
- * Name of the {@code X-Android-APK-Signer} attribute.
- */
- private static final String SIGNATURE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
-
- /**
- * Value of the {@code X-Android-APK-Signer} attribute when the APK is signed with the v2
- * scheme.
- */
- public static final String SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED = "2";
-
- /**
- * Files to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_FILES = Sets.newHashSet(
- ManifestGenerationExtension.MANIFEST_NAME, SIGNATURE_FILE);
-
- /**
- * Same as {@link #IGNORED_FILES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_FILES_LC = Sets.newHashSet(
- IGNORED_FILES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
-
- /**
- * Prefix of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_PREFIXES = Sets.newHashSet(
- "SIG-");
-
- /**
- * Same as {@link #IGNORED_PREFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_PREFIXES_LC = Sets.newHashSet(
- IGNORED_PREFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Suffixes of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_SUFFIXES = Sets.newHashSet(
- ".SF", ".DSA", ".RSA", ".EC");
-
- /**
- * Same as {@link #IGNORED_SUFFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_SUFFIXES_LC = Sets.newHashSet(
- IGNORED_SUFFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Extension maintaining the manifest.
- */
- @Nonnull
- private final ManifestGenerationExtension mManifestExtension;
-
- /**
- * Message digest to use.
- */
- @Nonnull
- private final MessageDigest mMessageDigest;
-
- /**
- * Signature file. Note that the signature file is itself a manifest file but it is
- * a different one from the "standard" MANIFEST.MF.
- */
- @Nonnull
- private final Manifest mSignatureFile;
-
- /**
- * Has the signature manifest been changed?
- */
- private boolean mDirty;
-
- /**
- * Signer certificate.
- */
- @Nonnull
- private final X509Certificate mCertificate;
-
- /**
- * The private key used to sign the jar.
- */
- @Nonnull
- private final PrivateKey mPrivateKey;
-
- /**
- * Algorithm with which .SF file is signed.
- */
- @Nonnull
- private final SignatureAlgorithm mSignatureAlgorithm;
-
- /**
- * Digest algorithm to use for MANIFEST.MF and contents of APK entries.
- */
- @Nonnull
- private final DigestAlgorithm mDigestAlgorithm;
-
- /**
- * Value to output for the {@code X-Android-APK-Signed} header or {@code null} if the header
- * should not be output.
- */
- @Nullable
- private final String mApkSignedHeaderValue;
-
- /**
- * The extension registered with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new signature extension.
- *
- * @param manifestExtension the extension maintaining the manifest
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- * @param apkSignedHeaderValue value of the {@code X-Android-APK-Signed} header to output into
- * the {@code .SF} file or {@code null} if the header should not be output.
- *
- * @throws NoSuchAlgorithmException failed to obtain the digest algorithm.
- */
- public SignatureExtension(@Nonnull ManifestGenerationExtension manifestExtension,
- int minSdkVersion, @Nonnull X509Certificate certificate, @Nonnull PrivateKey privateKey,
- @Nullable String apkSignedHeaderValue)
- throws NoSuchAlgorithmException {
- mManifestExtension = manifestExtension;
- mSignatureFile = new Manifest();
- mDirty = false;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mApkSignedHeaderValue = apkSignedHeaderValue;
-
- mSignatureAlgorithm =
- SignatureAlgorithm.fromKeyAlgorithm(privateKey.getAlgorithm(), minSdkVersion);
- mDigestAlgorithm = DigestAlgorithm.findBest(minSdkVersion, mSignatureAlgorithm);
- mMessageDigest = MessageDigest.getInstance(mDigestAlgorithm.messageDigestName);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the
- * {@link ManifestGenerationExtension}. Note that the {@code ManifestGenerationExtension}
- * needs to be registered as a precondition for this method.
- *
- * @throws IOException failed to analyze the zip
- */
- public void register() throws IOException {
- Preconditions.checkState(mExtension == null, "register() already invoked");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() {
- return SignatureExtension.this::updateSignatureIfNeeded;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@Nonnull final StoredEntry entry,
- @Nullable final StoredEntry replaced) {
- if (replaced != null) {
- Preconditions.checkArgument(entry.getCentralDirectoryHeader().getName().equals(
- replaced.getCentralDirectoryHeader().getName()));
- }
-
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> {
- if (replaced != null) {
- SignatureExtension.this.removed(replaced);
- }
-
- SignatureExtension.this.added(entry);
- };
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@Nonnull final StoredEntry entry) {
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> SignatureExtension.this.removed(entry);
- }
- };
-
- mManifestExtension.zFile().addZFileExtension(mExtension);
- readSignatureFile();
- }
-
- /**
- * Reads the signature file (if any) on the zip file.
- * <p>
- * When this method terminates, we have the following guarantees:
- * <ul>
- * <li>An internal signature manifest exists.</li>
- * <li>All entries in the in-memory signature file exist in the zip file.</li>
- * <li>All entries in the zip file (with the exception of the signature-related files,
- * as specified by https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html)
- * exist in the in-memory signature file.</li>
- * <li>All entries in the in-memory signature file have digests that match their
- * contents in the zip.</li>
- * <li>All entries in the in-memory signature manifest exist also in the manifest file
- * and the digests are the same.</li>
- * <li>The main attributes of the in-memory signature manifest are valid. The manifest's
- * digest has not been verified and may not even exist.</li>
- * <li>If the internal in-memory signature manifest differs in any way from the one
- * written in the file, {@link #mDirty} will be set to {@code true}. Otherwise,
- * {@link #mDirty} will be set to {@code false}.</li>
- * </ul>
- *
- * @throws IOException failed to read the signature file
- */
- private void readSignatureFile() throws IOException {
- boolean needsNewSignature = false;
-
- StoredEntry signatureEntry = mManifestExtension.zFile().get(SIGNATURE_FILE);
- if (signatureEntry != null) {
- byte[] signatureData = signatureEntry.read();
- mSignatureFile.read(new ByteArrayInputStream(signatureData));
-
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
- String versionName = mainAttrs.getValue(SIGNATURE_VERSION_NAME);
- String createdBy = mainAttrs.getValue(SIGNATURE_CREATED_BY_NAME);
- String apkSigned = mainAttrs.getValue(SIGNATURE_ANDROID_APK_SIGNED_NAME);
-
- if (!SIGNATURE_VERSION_VALUE.equals(versionName)
- || !SIGNATURE_CREATED_BY_VALUE.equals(createdBy)
- || mainAttrs.getValue(mDigestAlgorithm.manifestAttributeName) == null
- || !Objects.equal(mApkSignedHeaderValue, apkSigned)) {
- needsNewSignature = true;
- }
- } else {
- needsNewSignature = true;
- }
-
- if (needsNewSignature) {
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
-
- mainAttrs.putValue(SIGNATURE_CREATED_BY_NAME, SIGNATURE_CREATED_BY_VALUE);
- mainAttrs.putValue(SIGNATURE_VERSION_NAME, SIGNATURE_VERSION_VALUE);
- if (mApkSignedHeaderValue != null) {
- mainAttrs.putValue(SIGNATURE_ANDROID_APK_SIGNED_NAME, mApkSignedHeaderValue);
- } else {
- mainAttrs.remove(SIGNATURE_ANDROID_APK_SIGNED_NAME);
- }
-
- mDirty = true;
- }
-
- /*
- * At this point we have a valid in-memory signature file with a valid header. mDirty
- * states whether this is the same as the file-based signature file.
- *
- * Now, check we have the same files in the zip as in the signature file and that all
- * digests match. While we do this, make sure the manifest is also up-do-date.
- *
- * We ignore all signature-related files that exist in the zip that are signature-related.
- * This are defined in the jar format specification.
- */
- Set<StoredEntry> allEntries =
- mManifestExtension.zFile().entries().stream()
- .filter(se -> !isIgnoredFile(se.getCentralDirectoryHeader().getName()))
- .collect(Collectors.toSet());
-
- Set<String> sigEntriesToRemove = Sets.newHashSet(mSignatureFile.getEntries().keySet());
- Set<String> manEntriesToRemove = Sets.newHashSet(mManifestExtension.allEntries().keySet());
- for (StoredEntry se : allEntries) {
- /*
- * Update the entry's digest, if needed.
- */
- setDigestForEntry(se);
-
- /*
- * This entry exists in the file, so remove it from the list of entries to remove
- * from the manifest and signature file.
- */
- sigEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- manEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- }
-
- for (String toRemoveInSignature : sigEntriesToRemove) {
- mSignatureFile.getEntries().remove(toRemoveInSignature);
- mDirty = true;
- }
-
- for (String toRemoveInManifest : manEntriesToRemove) {
- mManifestExtension.removeEntry(toRemoveInManifest);
- }
- }
-
- /**
- * This method will recompute the manifest's digest and will update the signature file if the
- * manifest has changed. It then writes the signature file, if dirty for any reason (including
- * from recomputing the manifest's digest).
- *
- * @throws IOException failed to read / write zip data
- */
- private void updateSignatureIfNeeded() throws IOException {
- byte[] manifestData = mManifestExtension.getManifestBytes();
- byte[] manifestDataDigest = mMessageDigest.digest(manifestData);
-
-
- String manifestDataDigestTxt = Base64.getEncoder().encodeToString(manifestDataDigest);
-
- if (!manifestDataDigestTxt.equals(mSignatureFile.getMainAttributes().getValue(
- mDigestAlgorithm.manifestAttributeName))) {
- mSignatureFile
- .getMainAttributes()
- .putValue(mDigestAlgorithm.manifestAttributeName, manifestDataDigestTxt);
- mDirty = true;
- }
-
- if (!mDirty) {
- return;
- }
-
- ByteArrayOutputStream signatureBytes = new ByteArrayOutputStream();
- mSignatureFile.write(signatureBytes);
-
- mManifestExtension.zFile().add(
- SIGNATURE_FILE,
- new ByteArrayInputStream(signatureBytes.toByteArray()));
-
- String digitalSignatureFile = SIGNATURE_BASE + "." + mPrivateKey.getAlgorithm();
- try {
- mManifestExtension.zFile().add(
- digitalSignatureFile,
- new ByteArrayInputStream(computePkcs7Signature(signatureBytes.toByteArray())));
- } catch (CertificateEncodingException | OperatorCreationException | CMSException e) {
- throw new IOException("Failed to digitally sign signature file.", e);
- }
-
- mDirty = false;
- }
-
- /**
- * A new file has been added.
- *
- * @param entry the entry added
- * @throws IOException failed to add the entry to the signature file (or failed to compute the
- * entry's signature)
- */
- private void added(@Nonnull StoredEntry entry) throws IOException {
- setDigestForEntry(entry);
- }
-
- /**
- * Adds / updates the signature for an entry. If this entry has no signature, or its digest
- * doesn't match the one in the signature file (or manifest), it will be updated.
- *
- * @param entry the entry
- * @throws IOException failed to compute the entry's digest
- */
- private void setDigestForEntry(@Nonnull StoredEntry entry) throws IOException {
- String entryName = entry.getCentralDirectoryHeader().getName();
- byte[] entryDigestArray = mMessageDigest.digest(entry.read());
- String entryDigest = Base64.getEncoder().encodeToString(entryDigestArray);
-
- Attributes signatureAttributes = mSignatureFile.getEntries().get(entryName);
- if (signatureAttributes == null) {
- signatureAttributes = new Attributes();
- mSignatureFile.getEntries().put(entryName, signatureAttributes);
- mDirty = true;
- }
-
- if (!entryDigest.equals(signatureAttributes.getValue(
- mDigestAlgorithm.entryAttributeName))) {
- signatureAttributes.putValue(mDigestAlgorithm.entryAttributeName, entryDigest);
- mDirty = true;
- }
-
- /*
- * setAttribute will not mark the manifest as changed if the attribute is already there
- * and with the same value.
- */
- mManifestExtension.setAttribute(entryName, mDigestAlgorithm.entryAttributeName,
- entryDigest);
- }
-
- /**
- * File has been removed.
- *
- * @param entry the entry removed
- */
- private void removed(@Nonnull StoredEntry entry) {
- mSignatureFile.getEntries().remove(entry.getCentralDirectoryHeader().getName());
- mManifestExtension.removeEntry(entry.getCentralDirectoryHeader().getName());
- mDirty = true;
- }
-
- /**
- * Checks if a file should be ignored when signing.
- *
- * @param name the file name
- * @return should it be ignored
- */
- public static boolean isIgnoredFile(@Nonnull String name) {
- String metaInfPfx = ManifestGenerationExtension.META_INF_DIR + "/";
- boolean inMetaInf = name.startsWith(metaInfPfx)
- && !name.substring(metaInfPfx.length()).contains("/");
-
- /*
- * Only files in META-INF can be ignored. Files in sub-directories of META-INF are not
- * ignored.
- */
- if (!inMetaInf) {
- return false;
- }
-
- String nameLc = name.toLowerCase(Locale.US);
-
- /*
- * All files with names that match (case insensitive) the ignored list are ignored.
- */
- if (IGNORED_FILES_LC.contains(nameLc)) {
- return true;
- }
-
- for (String pfx : IGNORED_PREFIXES_LC) {
- if (nameLc.startsWith(pfx)) {
- return true;
- }
- }
-
- for (String sfx : IGNORED_SUFFIXES_LC) {
- if (nameLc.endsWith(sfx)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Computes the digital signature of an array of data.
- *
- * @param data the data
- * @return the digital signature
- * @throws IOException failed to read/write signature data
- * @throws CertificateEncodingException failed to sign the data
- * @throws OperatorCreationException failed to sign the data
- * @throws CMSException failed to sign the data
- */
- private byte[] computePkcs7Signature(@Nonnull byte[] data) throws IOException,
- CertificateEncodingException, OperatorCreationException, CMSException {
- CMSProcessableByteArray cmsData = new CMSProcessableByteArray(data);
-
- ArrayList<X509Certificate> certList = new ArrayList<>();
- certList.add(mCertificate);
- JcaCertStore certs = new JcaCertStore(certList);
-
- CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
- String signatureAlgName = mSignatureAlgorithm.signatureAlgorithmName(mDigestAlgorithm);
- ContentSigner shaSigner =
- new JcaContentSignerBuilder(signatureAlgName).build(mPrivateKey);
- gen.addSignerInfoGenerator(
- new JcaSignerInfoGeneratorBuilder(
- new JcaDigestCalculatorProviderBuilder()
- .build())
- .setDirectSignature(true)
- .build(shaSigner, mCertificate));
- gen.addCertificates(certs);
- CMSSignedData sigData = gen.generate(cmsData, false);
-
- ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
-
- /*
- * DEROutputStream is not closeable! OMG!
- */
- DEROutputStream dos = null;
- try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
- dos = new DEROutputStream(outputBytes);
- dos.writeObject(asn1.readObject());
-
- DEROutputStream toClose = dos;
- dos = null;
- toClose.close();
- } catch (IOException e) {
- if (dos != null) {
- try {
- dos.close();
- } catch (IOException ee) {
- e.addSuppressed(ee);
- }
- }
- }
-
- return outputBytes.toByteArray();
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
new file mode 100644
index 0000000..2673bed
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
@@ -0,0 +1,392 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.ApkSignerEngine;
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.DefaultApkSignerEngine;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ZFile} extension which signs the APK.
+ *
+ * <p>
+ * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
+ * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
+ * extension's constructor.
+ */
+public class SigningExtension {
+ // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
+ // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
+ // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
+ // extension events/callbacks.
+ //
+ // The main issue leading to additional complexity in this class is that the current build
+ // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
+ // matter) for incremental builds. Thus:
+ // * ZFile extension receives no events for JAR entries already in the APK whereas
+ // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
+ // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
+ // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
+ // entries which the incremental build session did not touch.
+ // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
+ // from it whereas ApkSignerEngine produces no output only if it has already produced a signed
+ // APK and no changes have since been made to it. This class addresses this issue by checking
+ // in its "register" method whether the APK is correctly signed and, only if that's the case,
+ // doesn't modify the APK unless a JAR entry is added to it or removed from it after
+ // "register".
+
+ /**
+ * Minimum API Level on which this APK is supposed to run.
+ */
+ private final int mMinSdkVersion;
+
+ /**
+ * Whether JAR signing (aka v1 signing) is enabled.
+ */
+ private final boolean mV1SigningEnabled;
+
+ /**
+ * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
+ */
+ private final boolean mV2SigningEnabled;
+
+ /**
+ * Certificate of the signer, to be embedded into the APK's signature.
+ */
+ @Nonnull
+ private final X509Certificate mCertificate;
+
+ /**
+ * APK signer which performs most of the heavy lifting.
+ */
+ @Nonnull
+ private final ApkSignerEngine mSigner;
+
+ /**
+ * Names of APK entries which have been processed by {@link #mSigner}.
+ */
+ private final Set<String> mSignerProcessedOutputEntryNames = new HashSet<>();
+
+ /**
+ * Cached contents of the most recently output APK Signing Block or {@code null} if the block
+ * hasn't yet been output.
+ */
+ @Nullable
+ private byte[] mCachedApkSigningBlock;
+
+ /**
+ * {@code true} if signatures may need to be output, {@code false} if there's no need to output
+ * signatures. This is used in an optimization where we don't modify the APK if it's already
+ * signed and if no JAR entries have been added to or removed from the file.
+ */
+ private boolean mDirty;
+
+ /**
+ * The extension registered with the {@link ZFile}. {@code null} if not registered.
+ */
+ @Nullable
+ private ZFileExtension mExtension;
+
+ /**
+ * The file this extension is attached to. {@code null} if not yet registered.
+ */
+ @Nullable
+ private ZFile mZFile;
+
+ public SigningExtension(
+ int minSdkVersion,
+ @Nonnull X509Certificate certificate,
+ @Nonnull PrivateKey privateKey,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled) throws InvalidKeyException {
+ DefaultApkSignerEngine.SignerConfig signerConfig =
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ "CERT", privateKey, ImmutableList.of(certificate)).build();
+ mSigner =
+ new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
+ .setOtherSignersSignaturesPreserved(false)
+ .setV1SigningEnabled(v1SigningEnabled)
+ .setV2SigningEnabled(v2SigningEnabled)
+ .setCreatedBy("1.0 (Android)")
+ .build();
+ mMinSdkVersion = minSdkVersion;
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mCertificate = certificate;
+ }
+
+ public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
+ Preconditions.checkState(mExtension == null, "register() already invoked");
+ mZFile = zFile;
+ mDirty = !isCurrentSignatureAsRequested();
+ mExtension = new ZFileExtension() {
+ @Override
+ public IOExceptionRunnable added(
+ @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
+ return () -> onZipEntryOutput(entry);
+ }
+
+ @Override
+ public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ return () -> onZipEntryRemovedFromOutput(entryName);
+ }
+
+ @Override
+ public IOExceptionRunnable beforeUpdate() throws IOException {
+ return () -> onOutputZipReadyForUpdate();
+ }
+
+ @Override
+ public void entriesWritten() throws IOException {
+ onOutputZipEntriesWritten();
+ }
+
+ @Override
+ public void closed() {
+ onOutputClosed();
+ }
+ };
+ mZFile.addZFileExtension(mExtension);
+ }
+
+ /**
+ * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
+ * extension.
+ */
+ private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
+ ApkVerifier.Result result;
+ try {
+ result =
+ new ApkVerifier.Builder(new ZFileDataSource(mZFile))
+ .setMinCheckedPlatformVersion(mMinSdkVersion)
+ .build()
+ .verify();
+ } catch (ApkFormatException e) {
+ // Malformed APK
+ return false;
+ }
+
+ if (!result.isVerified()) {
+ // Signature(s) did not verify
+ return false;
+ }
+
+ if ((result.isVerifiedUsingV1Scheme() != mV1SigningEnabled)
+ || (result.isVerifiedUsingV2Scheme() != mV2SigningEnabled)) {
+ // APK isn't signed with exactly the schemes we want it to be signed
+ return false;
+ }
+
+ List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
+ if (verifiedSignerCerts.size() != 1) {
+ // APK is not signed by exactly one signer
+ return false;
+ }
+
+ byte[] expectedEncodedCert;
+ byte[] actualEncodedCert;
+ try {
+ expectedEncodedCert = mCertificate.getEncoded();
+ actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ // Failed to encode signing certificates
+ return false;
+ }
+
+ if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
+ // APK is signed by a wrong signer
+ return false;
+ }
+
+ // APK is signed the way we want it to be signed
+ return true;
+ }
+
+ private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
+ setDirty();
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ // This event may arrive after the entry has already been deleted. In that case, we don't
+ // report the addition of the entry to ApkSignerEngine.
+ if (entry.isDeleted()) {
+ return;
+ }
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ mSigner.outputJarEntry(entryName);
+ mSignerProcessedOutputEntryNames.add(entryName);
+ if (inspectEntryRequest != null) {
+ byte[] entryContents = entry.read();
+ inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
+ inspectEntryRequest.done();
+ }
+ }
+
+ private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
+ setDirty();
+ mSigner.outputJarEntryRemoved(entryName);
+ mSignerProcessedOutputEntryNames.remove(entryName);
+ }
+
+ private void onOutputZipReadyForUpdate() throws IOException {
+ if (!mDirty) {
+ return;
+ }
+
+ // Notify signer engine about ZIP entries that have appeared in the output without the
+ // engine knowing. Also identify ZIP entries which disappeared from the output without the
+ // engine knowing.
+ Set<String> unprocessedRemovedEntryNames = new HashSet<>(mSignerProcessedOutputEntryNames);
+ for (StoredEntry entry : mZFile.entries()) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ unprocessedRemovedEntryNames.remove(entryName);
+ if (!mSignerProcessedOutputEntryNames.contains(entryName)) {
+ // Signer engine is not yet aware that this entry is in the output
+ onZipEntryOutput(entry);
+ }
+ }
+
+ // Notify signer engine about entries which disappeared from the output without the engine
+ // knowing
+ for (String entryName : unprocessedRemovedEntryNames) {
+ onZipEntryRemovedFromOutput(entryName);
+ }
+
+ // Check whether we need to output additional JAR entries which comprise the v1 signature
+ ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
+ try {
+ addV1SignatureRequest = mSigner.outputJarEntries();
+ } catch (Exception e) {
+ throw new IOException("Failed to generate v1 signature", e);
+ }
+ if (addV1SignatureRequest == null) {
+ return;
+ }
+
+ // We need to output additional JAR entries which comprise the v1 signature
+ List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
+ new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
+
+ // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
+ // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
+ // ManifestGenerationExtension.
+ for (int i = 0; i < v1SignatureEntries.size(); i++) {
+ ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
+ String name = entry.getName();
+ if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
+ continue;
+ }
+ if (i != 0) {
+ v1SignatureEntries.remove(i);
+ v1SignatureEntries.add(0, entry);
+ }
+ break;
+ }
+
+ // Output the JAR entries comprising the v1 signature
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
+ String name = entry.getName();
+ byte[] data = entry.getData();
+ mZFile.add(name, new ByteArrayInputStream(data));
+ }
+
+ addV1SignatureRequest.done();
+ }
+
+ private void onOutputZipEntriesWritten() throws IOException {
+ if (!mDirty) {
+ return;
+ }
+
+ // Check whether we should output an APK Signing Block which contains v2 signatures
+ byte[] apkSigningBlock;
+ byte[] centralDirBytes = mZFile.getCentralDirectoryBytes();
+ byte[] eocdBytes = mZFile.getEocdBytes();
+ ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
+ // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
+ // cache the block to speed things up. The cached block is invalidated by any changes to the
+ // file (as reported to this extension).
+ if (mCachedApkSigningBlock != null) {
+ apkSigningBlock = mCachedApkSigningBlock;
+ addV2SignatureRequest = null;
+ } else {
+ DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
+ DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
+ long zipEntriesSizeBytes =
+ mZFile.getCentralDirectoryOffset() - mZFile.getExtraDirectoryOffset();
+ DataSource zipEntries = new ZFileDataSource(mZFile, 0, zipEntriesSizeBytes);
+ try {
+ addV2SignatureRequest = mSigner.outputZipSections(zipEntries, centralDir, eocd);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
+ | ApkFormatException | IOException e) {
+ throw new IOException("Failed to generate v2 signature", e);
+ }
+ apkSigningBlock =
+ (addV2SignatureRequest != null)
+ ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
+ mCachedApkSigningBlock = apkSigningBlock;
+ }
+
+ // Insert the APK Signing Block into the output right before the ZIP Central Directory and
+ // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
+ // Directory.
+ mZFile.directWrite(
+ mZFile.getCentralDirectoryOffset() - mZFile.getExtraDirectoryOffset(),
+ apkSigningBlock);
+ mZFile.setExtraDirectoryOffset(apkSigningBlock.length);
+
+ if (addV2SignatureRequest != null) {
+ addV2SignatureRequest.done();
+ }
+ }
+
+ private void onOutputClosed() {
+ if (!mDirty) {
+ return;
+ }
+ mSigner.outputDone();
+ mDirty = false;
+ }
+
+ private void setDirty() {
+ mDirty = true;
+ mCachedApkSigningBlock = null;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
new file mode 100644
index 0000000..c42db12
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
@@ -0,0 +1,157 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apkzlib.zip.ZFile;
+import com.google.common.base.Preconditions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
+
+/**
+ * {@link DataSource} backed by contents of {@link ZFile}.
+ */
+class ZFileDataSource implements DataSource {
+
+ private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+ @Nonnull
+ private final ZFile mFile;
+
+ /**
+ * Offset (in bytes) relative to the start of file where the region visible in this data source
+ * starts.
+ */
+ private final long mOffset;
+
+ /**
+ * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole
+ * file is visible in this data source and thus its size may change if the file's size changes.
+ */
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to
+ * the contents of the file, including the size of the file, will be visible in this data
+ * source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file) {
+ mFile = file;
+ mOffset = 0;
+ mSize = -1;
+ }
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region
+ * of the provided file. Changes to the contents of this region of the file will be visible in
+ * this data source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file, long offset, long size) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ mFile = file;
+ mOffset = offset;
+ mSize = size;
+ }
+
+ @Override
+ public long size() {
+ if (mSize == -1) {
+ // Data source size is the current size of the file
+ try {
+ return mFile.directSize();
+ } catch (IOException e) {
+ return 0;
+ }
+ } else {
+ // Data source size is fixed
+ return mSize;
+ }
+ }
+
+ @Override
+ public DataSource slice(long offset, long size) {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if ((offset == 0) && (size == sourceSize)) {
+ return this;
+ }
+
+ return new ZFileDataSource(mFile, mOffset + offset, size);
+ }
+
+ @Override
+ public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long chunkOffsetInFile = mOffset + offset;
+ long remaining = size;
+ byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
+ while (remaining > 0) {
+ int chunkSize = (int) Math.min(remaining, buf.length);
+ int readSize = mFile.directRead(chunkOffsetInFile, buf, 0, chunkSize);
+ if (readSize == -1) {
+ throw new EOFException("Premature EOF");
+ }
+ if (readSize > 0) {
+ sink.consume(buf, 0, readSize);
+ chunkOffsetInFile += readSize;
+ remaining -= readSize;
+ }
+ }
+ }
+
+ @Override
+ public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ int prevLimit = dest.limit();
+ try {
+ mFile.directFullyRead(mOffset + offset, dest);
+ } finally {
+ dest.limit(prevLimit);
+ }
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+ ByteBuffer result = ByteBuffer.allocate(size);
+ copyTo(offset, size, result);
+ result.flip();
+ return result;
+ }
+
+ private static void checkChunkValid(long offset, long size, long sourceSize) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize");
+ long endOffset = offset + size;
+ Preconditions.checkArgument(offset <= endOffset, "offset > endOffset");
+ Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize");
+ }
+}
diff --git a/src/main/java/com/android/apkzlib/sign/package-info.java b/src/main/java/com/android/apkzlib/sign/package-info.java
index bdcb02e..6bb692c 100644
--- a/src/main/java/com/android/apkzlib/sign/package-info.java
+++ b/src/main/java/com/android/apkzlib/sign/package-info.java
@@ -30,7 +30,7 @@ and will change the zip file itself.
<p>
The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will
ensure the zip has a manifest file and is, therefore, a valid jar.
-The {@link com.android.apkzlib.sign.SignatureExtension} extension will
+The {@link com.android.apkzlib.sign.SigningExtension} extension will
ensure the jar is signed.
<p>
The extension mechanism used is the one provided in the {@code zip} package (see
@@ -72,7 +72,7 @@ follows (if only the manifest generation extension was added to the {@code ZFile
<li>The zip is finally written with an updated manifest.</li>
</ol>
<p>
-To generate a signed apk (v1), we need to add a second extension, the {@code SignatureExtension}.
+To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.
This extension will also register listeners with the {@code ZFile}.
<p>
In this case the flow would be (starting a bit earlier for clarity and assuming a package task
@@ -85,9 +85,9 @@ in the build process):
<li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.</li>
<li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid
manifest. No changes are done to the {@code ZFile}.</li>
- <li>Package task creates a {@code SignatureExtension}.</li>
- <li>Package task registers the {@code SignatureExtension} with the {@code ZFile}.</li>
- <li>The {@code SignatureExtension} registers a {@code ZFileExtension} with the {@code ZFile}
+ <li>Package task creates a {@code SigningExtension}.</li>
+ <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.</li>
+ <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile}
and look at the {@code ZFile} to see if there is a valid signature file.</li>
<li>If there are changes to the digital signature file needed, these are marked internally in
the extension. If there are changes needed to the digests, the manifest is updated (by calling
@@ -100,7 +100,7 @@ in the build process):
<li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}
method of all registered extensions.</li>
<li>The {@code ManifestGenerationExtension} ignores added invocations.</li>
- <li>The {@code SignatureExtension} computes the digest for the added file and stores them in
+ <li>The {@code SigningExtension} computes the digest for the added file and stores them in
the manifest.<br>
<em>(when all files are added to the apk, all digests are computed and the manifest is updated
but only in memory; the apk file has not been touched; also note that {@code ZFile} has not
@@ -108,15 +108,15 @@ in the build process):
<li>Package task calls {@code ZFile.update()} to update the apk.</li>
<li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is
done before anything is written. In this case both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are invoked.</li>
+ {@code SigningExtension} are invoked.</li>
<li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest,
unless nothing has changed, in which case it does nothing.</li>
- <li>The {@code SignatureExtension} will add the SF file (unless nothing has changed), will
+ <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will
compute the digital signature of the SF file and write it to the {@code ZFile}.<br>
<em>(note that the order by which the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are called is non-deterministic; however, this is not a problem
+ {@code SigningExtension} are called is non-deterministic; however, this is not a problem
because the manifest is already computed by the {@code ManifestGenerationExtension} at this
- time and the {@code SignatureExtension} will obtain the manifest data from the
+ time and the {@code SigningExtension} will obtain the manifest data from the
{@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the
{@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is
irrelevant.)</em></li>
@@ -124,9 +124,8 @@ in the build process):
{@code ZFile.update()} method continues.</li>
<li>{@code ZFile.update()} writes all changes and new entries to the zip file.</li>
<li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all
- registered extensions. Both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} ignore this notification -- but the {@code FullApkSignExtension} will
- kick in at this point, if it has been created.</li>
+ registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature
+ has changed.</li>
<li>{@code ZFile} writes the central directory and EOCD.</li>
<li>{@code ZFile.update()} returns control to the package task.</li>
<li>The package task finishes.</li>
@@ -139,17 +138,15 @@ zip).</em>
<p>
If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1
signatures are correct, neither the {@code ManifestGenerationExtension} nor the
-{@code SignatureExtension} will not do anything on the {@code beforeUpdate()} and the
+{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the
{@code ZFile} won't even be open for writing.
<p>
This implementation provides perfect incremental updates.
<p>
Additionally, by adding/removing extensions we can configure what type of apk we want:
<ul>
- <li>No SignatureExtension &amp; No FullApkSignExtension ⇒ Aligned, unsigned apk.</li>
- <li>Signature Extension &amp; No FullApkSignExtension ⇒ Aligned, v1 only signed apk.</li>
- <li>Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v1 &amp; v2 signed apk.</li>
- <li>No Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v2 only signed apk.</li>
+ <li>No SigningExtension ⇒ Aligned, unsigned apk.</li>
+ <li>SigningExtension ⇒ Aligned, signed apk.
</ul>
So, by configuring which extensions to add, the package task can decide what type of apk we want.
*/
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java b/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java
deleted file mode 100644
index 71cf026..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.v2;
-
-import com.android.apkzlib.utils.ApkZLibPair;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-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.List;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Nonnull;
-
-/**
- * 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 ApkSignerV2 {
- /*
- * The two main goals of APK Signature Scheme v2 are:
- * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
- * cover every byte of the APK being signed.
- * 2. Enable much faster signature and integrity verification. This is achieved by requiring
- * only a minimal amount of APK parsing before the signature is verified, thus completely
- * bypassing ZIP entry decompression and by making integrity verification parallelizable by
- * employing a hash tree.
- *
- * The generated signature block is wrapped into an APK Signing Block and inserted into the
- * original APK immediately before the start of ZIP Central Directory. This is to ensure that
- * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
- * extensibility. For example, a future signature scheme could insert its signatures there as
- * well. The contract of the APK Signing Block is that all contents outside of the block must be
- * protected by signatures inside the block.
- */
-
- private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
- private static final byte[] APK_SIGNING_BLOCK_MAGIC =
- new byte[] {
- 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
- 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
- };
- private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
- private ApkSignerV2() {}
-
- /**
- * Signer configuration.
- */
- public static final class SignerConfig {
- /**
- * Private key.
- */
- @Nonnull
- public PrivateKey privateKey;
-
- /**
- * Certificates, with the first certificate containing the public key corresponding to
- * {@link #privateKey}.
- */
- @Nonnull
- public List<X509Certificate> certificates;
-
- /**
- * List of signature algorithms with which to sign. At least one algorithm must be
- * provided.
- */
- @Nonnull
- public List<SignatureAlgorithm> signatureAlgorithms;
- }
-
- /**
- * 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(
- @Nonnull 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 ImmutableList.of(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 ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
- }
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- // DSA is supported only with SHA-256.
- return ImmutableList.of(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 ImmutableList.of(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 ImmutableList.of(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.
- *
- * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
- * of META-INF/*.SF files of APK being signed must contain the
- * {@code X-Android-APK-Signed: 2} attribute.
- *
- * @param signerConfigs signer configurations, one for each signer. At least one configuration
- * 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
- */
- @Nonnull
- public static byte[] generateApkSigningBlock(
- @Nonnull DigestSource beforeCentralDir,
- @Nonnull DigestSource centralDir,
- @Nonnull DigestSource eocd,
- @Nonnull List<SignerConfig> signerConfigs)
- throws 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 = Sets.newHashSetWithExpectedSize(1);
- for (SignerConfig signerConfig : signerConfigs) {
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
- }
- }
-
- // Compute digests of APK contents.
- Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
- try {
- contentDigests =
- computeContentDigests(
- contentDigestAlgorithms,
- new DigestSource[] {beforeCentralDir, centralDir, eocd});
- } catch (DigestException e) {
- throw new SignatureException("Failed to compute digests of APK", e);
- }
-
- // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
- return generateApkSigningBlock(signerConfigs, contentDigests);
- }
-
- @Nonnull
- private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
- @Nonnull Set<ContentDigestAlgorithm> digestAlgorithms,
- @Nonnull DigestSource[] contents) throws DigestException {
- // For each digest algorithm the result is computed as follows:
- // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
- // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
- // No chunks are produced for empty (zero length) segments.
- // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
- // length in bytes (uint32 little-endian) and the chunk's contents.
- // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
- // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
- // segments in-order.
-
- long chunkCountLong = 0;
- for (DigestSource 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 jcaAlgorithmName = digestAlgorithm.getJcaMessageDigestAlgorithmName();
- try {
- mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(jcaAlgorithmName + " MessageDigest not supported", e);
- }
- }
-
- 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 (DigestSource 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.feedDigests(inputOffset, chunkSize, mds);
- } catch (IOException e) {
- throw new DigestException("Failed to digest 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 =
- Maps.newHashMapWithExpectedSize(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 = Lists.newArrayListWithExpectedSize(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<ApkZLibPair<Integer, byte[]>> digests =
- Lists.newArrayListWithExpectedSize(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(new ApkZLibPair<>(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 =
- Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
- signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
- String jcaSignatureAlgorithm = sigAlgAndParams.v1;
- AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.v2;
- 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(new ApkZLibPair<>(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<ApkZLibPair<Integer, byte[]>> signatures;
- public byte[] publicKey;
- }
-
- private static final class SignedData {
- public List<ApkZLibPair<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 = Lists.newArrayListWithExpectedSize(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<ApkZLibPair<Integer, byte[]>> sequence) {
- int resultSize = 0;
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- resultSize += 12 + element.v2.length;
- }
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- byte[] second = element.v2;
- result.putInt(8 + second.length);
- result.putInt(element.v1);
- result.putInt(second.length);
- result.put(second);
- }
- return result.array();
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java
deleted file mode 100644
index cd2ed1d..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.v2;
-
-import com.google.common.base.Preconditions;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * {@code byte[]} which is fed into {@link MessageDigest} instances.
- */
-public class ByteArrayDigestSource implements DigestSource {
- private final byte[] mBuf;
-
- /**
- * Constructs a new {@code ByteArrayDigestSource} instance which obtains its data from the
- * provided byte array. Changes to the byte array's contents are reflected visible in this
- * source.
- */
- public ByteArrayDigestSource(@Nonnull byte[] buf) {
- mBuf = buf;
- }
-
- @Override
- public long size() {
- return mBuf.length;
- }
-
- @Override
- public void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mBuf.length, "offset too large: %s", offset);
- int offsetInBuf = (int) offset;
- Preconditions.checkPositionIndex(offsetInBuf, mBuf.length, "offset out of range");
- int availableSize = mBuf.length - offsetInBuf;
- Preconditions.checkArgument(
- size <= availableSize,
- "offset (%s) + size (%s) > array length (%s)", offset, size, mBuf.length);
-
- for (MessageDigest md : digests) {
- md.update(mBuf, offsetInBuf, size);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java
deleted file mode 100644
index 46678e4..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.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 mJcaMessageDigestAlgorithmName;
- private final int mChunkDigestOutputSizeBytes;
-
- private ContentDigestAlgorithm(
- String jcaMessageDigestAlgorithmName, int chunkDigestOutputSizeBytes) {
- mJcaMessageDigestAlgorithmName = jcaMessageDigestAlgorithmName;
- mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
- }
-
- /**
- * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
- * chunks by this content digest algorithm.
- */
- String getJcaMessageDigestAlgorithmName() {
- return mJcaMessageDigestAlgorithmName;
- }
-
- /**
- * 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/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java
deleted file mode 100644
index 4a9cc19..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * Source of data which is fed into {@link MessageDigest} instances.
- *
- * <p>This abstraction serves two purposes:
- * <ul>
- * <li>Transparent digesting of different types of sources, such as {@code byte[]},
- * {@code ZFile}, {@link java.nio.ByteBuffer} and/or memory-mapped file.</li>
- * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
- * would have worked as the unifying abstraction.</li>
- * </ul>
- */
-public interface DigestSource {
- /**
- * Returns the amount of data (in bytes) contained in this data source.
- */
- long size();
-
- /**
- * Feeds the specified chunk from this data source into each of the provided
- * {@link MessageDigest} instances. Each {@code MessageDigest} instance receives the specified
- * chunk of data in full.
- *
- * @param offset index (in bytes) at which the chunk starts relative to the start of this data
- * source.
- * @param size size (in bytes) of the chunk.
- */
- void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests) throws IOException;
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java
deleted file mode 100644
index b922d8a..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.v2;
-
-import com.android.apkzlib.utils.ApkZLibPair;
-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,
- new ApkZLibPair<>("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,
- new ApkZLibPair<>(
- "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,
- new ApkZLibPair<>("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,
- new ApkZLibPair<>("SHA512withRSA", null)),
-
- /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- ECDSA_WITH_SHA256(
- 0x0201,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withECDSA", null)),
-
- /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
- ECDSA_WITH_SHA512(
- 0x0202,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>("SHA512withECDSA", null)),
-
- /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- DSA_WITH_SHA256(
- 0x0301,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withDSA", null));
-
- private final int mId;
- private final ContentDigestAlgorithm mContentDigestAlgorithm;
- private final ApkZLibPair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
-
- private SignatureAlgorithm(int id,
- ContentDigestAlgorithm contentDigestAlgorithm,
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
- mId = id;
- mContentDigestAlgorithm = contentDigestAlgorithm;
- 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 {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
- * (or null if not needed) to parameterize the {@code Signature}.
- */
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
- return mJcaSignatureAlgAndParams;
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java
deleted file mode 100644
index 31ae812..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apkzlib.sign.v2;
-
-import com.android.apkzlib.zip.ZFile;
-import com.google.common.base.Preconditions;
-import java.io.IOException;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * Contiguous section of {@link ZFile} which is fed into {@link MessageDigest} instances.
- */
-public class ZFileDigestSource implements DigestSource {
- private final ZFile mFile;
- private final long mOffset;
- private final long mSize;
-
- /**
- * Constructs a new {@code ZFileDigestSource} representing the section of the file starting
- * at the provided {@code offset} and extending for the provided {@code size} number of bytes.
- */
- public ZFileDigestSource(@Nonnull ZFile file, long offset, long size) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- mFile = file;
- mOffset = offset;
- mSize = size;
- }
-
-
- @Override
- public long size() {
- return mSize;
- }
-
- @Override
- public void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests)
- throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mSize, "offset: %s, file size: %s", offset, mSize);
- long chunkStartOffset = mOffset + offset;
- long availableSize = mSize - offset;
- Preconditions.checkArgument(
- size <= availableSize, "offset: %s, size: %s, file size: %s", offset, size, mSize);
-
- byte[] chunk = new byte[size];
- mFile.directFullyRead(chunkStartOffset, chunk);
- for (MessageDigest md : digests) {
- md.update(chunk);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/zfile/ZFiles.java b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
index fdf6983..d5102a6 100644
--- a/src/main/java/com/android/apkzlib/zfile/ZFiles.java
+++ b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
@@ -16,12 +16,10 @@
package com.android.apkzlib.zfile;
-import com.android.apkzlib.sign.FullApkSignExtension;
import com.android.apkzlib.sign.ManifestGenerationExtension;
-import com.android.apkzlib.sign.SignatureExtension;
+import com.android.apkzlib.sign.SigningExtension;
import com.android.apkzlib.zip.AlignmentRule;
import com.android.apkzlib.zip.AlignmentRules;
-import com.android.apkzlib.zip.StoredEntry;
import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileOptions;
import java.io.File;
@@ -118,35 +116,12 @@ public class ZFiles {
if (key != null && certificate != null) {
try {
- if (v1SigningEnabled) {
- String apkSignedHeaderValue =
- (v2SigningEnabled)
- ? SignatureExtension
- .SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED
- : null;
- SignatureExtension jarSignatureSchemeExt = new SignatureExtension(manifestExt,
- minSdkVersion, certificate, key,
- apkSignedHeaderValue);
- jarSignatureSchemeExt.register();
- }
- if (v2SigningEnabled) {
- FullApkSignExtension apkSignatureSchemeV2Ext =
- new FullApkSignExtension(
- zfile,
- minSdkVersion,
- certificate,
- key);
- apkSignatureSchemeV2Ext.register();
- }
- if (!v1SigningEnabled) {
- // Remove v1 signature files from the APK
- for (StoredEntry entry : zfile.entries()) {
- if (SignatureExtension.isIgnoredFile(
- entry.getCentralDirectoryHeader().getName())) {
- entry.delete();
- }
- }
- }
+ new SigningExtension(
+ minSdkVersion,
+ certificate,
+ key,
+ v1SigningEnabled,
+ v2SigningEnabled).register(zfile);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException("Failed to create signature extensions", e);
}
diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
index 83b9ca3..a8ee56e 100644
--- a/src/main/java/com/android/apkzlib/zip/StoredEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
@@ -365,6 +365,13 @@ public class StoredEntry {
}
/**
+ * Returns {@code true} if this entry has been deleted/replaced.
+ */
+ public boolean isDeleted() {
+ return mDeleted;
+ }
+
+ /**
* Obtains the CDH associated with this entry.
*
* @return the CDH
diff --git a/src/main/java/com/android/apkzlib/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java
index e8798a5..301b942 100644
--- a/src/main/java/com/android/apkzlib/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -22,7 +22,6 @@ import com.android.apkzlib.utils.IOExceptionRunnable;
import com.android.apkzlib.zip.utils.ByteTracker;
import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.android.apkzlib.zip.utils.LittleEndianUtils;
-import com.android.apkzlib.zip.utils.RandomAccessFileUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
@@ -40,12 +39,14 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
+import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -810,7 +811,7 @@ public class ZFile implements Closeable {
/**
* Updates the file writing new entries and removing deleted entries. This will force
* reopening the file as read/write if the file wasn't open in read/write mode.
- *
+ *
* @throws IOException failed to update the file; this exception may have been thrown by
* the compressor but only reported here
*/
@@ -2068,6 +2069,22 @@ public class ZFile implements Closeable {
}
/**
+ * Returns the current size (in bytes) of the underlying file.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public long directSize() throws IOException {
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (mRaf == null) {
+ reopenRw();
+ assert mRaf != null;
+ }
+ return mRaf.length();
+ }
+
+ /**
* Directly reads data from the zip file. Invoking this method may force the zip to be reopened
* in read/write mode.
*
@@ -2081,17 +2098,30 @@ public class ZFile implements Closeable {
*/
public int directRead(long offset, @Nonnull byte[] data, int start, int count)
throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset < 0");
Preconditions.checkArgument(start >= 0, "start >= 0");
Preconditions.checkArgument(count >= 0, "count >= 0");
+ Preconditions.checkArgument(start <= data.length, "start > data.length");
+ Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
+ return directRead(offset, ByteBuffer.wrap(data, start, count));
+ }
- if (data.length == 0) {
+ /**
+ * Directly reads data from the zip file. Invoking this method may force the zip to be reopened
+ * in read/write mode.
+ *
+ * @param offset the offset from which data should be read
+ * @param dest the output buffer to fill with data from the {@code offset}.
+ * @return how many bytes of data have been written or {@code -1} if there are no more bytes
+ * to be read
+ * @throws IOException failed to write the data
+ */
+ public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+ if (!dest.hasRemaining()) {
return 0;
}
- Preconditions.checkArgument(start <= data.length, "start > data.length");
- Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
-
/*
* Only force a reopen if the file is closed.
*/
@@ -2101,7 +2131,7 @@ public class ZFile implements Closeable {
}
mRaf.seek(offset);
- return mRaf.read(data, start, count);
+ return mRaf.getChannel().read(dest);
}
/**
@@ -2116,7 +2146,7 @@ public class ZFile implements Closeable {
}
/**
- * Reads exactly @code data.length} bytes of data, failing if it was not possible to read all
+ * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all
* the requested data.
*
* @param offset the offset at which to start reading
@@ -2124,11 +2154,42 @@ public class ZFile implements Closeable {
* @throws IOException failed to read some data or there is not enough data to read
*/
public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException {
+ directFullyRead(offset, ByteBuffer.wrap(data));
+ }
+
+ /**
+ * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read
+ * all the requested data.
+ *
+ * @param offset the offset at which to start reading
+ * @param dest the output buffer to fill with data
+ * @throws IOException failed to read some data or there is not enough data to read
+ */
+ public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
Preconditions.checkArgument(offset >= 0, "offset < 0");
- Preconditions.checkNotNull(mRaf, "File is closed");
- mRaf.seek(offset);
- RandomAccessFileUtils.fullyRead(mRaf, data);
+ if (!dest.hasRemaining()) {
+ return;
+ }
+
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (mRaf == null) {
+ reopenRw();
+ assert mRaf != null;
+ }
+
+ FileChannel fileChannel = mRaf.getChannel();
+ while (dest.hasRemaining()) {
+ fileChannel.position(offset);
+ int chunkSize = fileChannel.read(dest);
+ if (chunkSize == -1) {
+ throw new EOFException(
+ "Failed to read " + dest.remaining() + " more bytes: premature EOF");
+ }
+ offset += chunkSize;
+ }
}
/**
diff --git a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
index d41978a..f72f63c 100644
--- a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
+++ b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
@@ -36,7 +36,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
/**
- * Tests that verify {@link FullApkSignExtension}.
+ * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}.
*/
public class FullApkSignTest {
@@ -63,9 +63,8 @@ public class FullApkSignTest {
* Generate a signed zip.
*/
ZFile zf = new ZFile(out, options);
- FullApkSignExtension signExtension =
- new FullApkSignExtension(zf, 13, signData.v2, signData.v1);
- signExtension.register();
+ new SigningExtension(13, signData.v2, signData.v1, false, true)
+ .register(zf);
String f1Name = "abc";
byte[] f1Data = new byte[] { 1, 1, 1, 1 };
zf.add(f1Name, new ByteArrayInputStream(f1Data));
diff --git a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
index 191bf53..ea48cfa 100644
--- a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
+++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
@@ -55,9 +55,7 @@ public class JarSigningTest {
ApkZLibPair<PrivateKey, X509Certificate> p =
SignatureTestUtils.generateSignaturePre18();
- SignatureExtension signatureExtension =
- new SignatureExtension(manifestExtension, 12, p.v2, p.v1, null);
- signatureExtension.register();
+ new SigningExtension(12, p.v2, p.v1, true, false).register(zf);
}
try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -85,7 +83,7 @@ public class JarSigningTest {
try (ZFile zf2 = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
me.register(zf2);
- new SignatureExtension(me, 10, p.v2, p.v1, null).register();
+ new SigningExtension(10, p.v2, p.v1, true, false).register(zf2);
}
try (ZFile zf3 = new ZFile(zipFile)) {
@@ -121,7 +119,7 @@ public class JarSigningTest {
Attributes signAttrs = signature.getAttributes("directory/file");
assertNotNull(signAttrs);
assertEquals(1, signAttrs.size());
- assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", signAttrs.getValue("SHA1-Digest"));
+ assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest"));
StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA");
assertNotNull(rsaEntry);
@@ -141,7 +139,7 @@ public class JarSigningTest {
try (ZFile zf2 = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
me.register(zf2);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
}
try (ZFile zf3 = new ZFile(zipFile)) {
@@ -178,7 +176,7 @@ public class JarSigningTest {
Attributes signAttrs = signature.getAttributes("directory/file");
assertNotNull(signAttrs);
assertEquals(1, signAttrs.size());
- assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=",
+ assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=",
signAttrs.getValue("SHA-256-Digest"));
StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC");
@@ -196,9 +194,7 @@ public class JarSigningTest {
ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
- FullApkSignExtension signatureExtension =
- new FullApkSignExtension(zf, 12, p.v2, p.v1);
- signatureExtension.register();
+ new SigningExtension(12, p.v2, p.v1, false, true).register(zf);
}
try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -227,7 +223,7 @@ public class JarSigningTest {
zf1.add(file1Name, new ByteArrayInputStream(file1Contents));
ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
me.register(zf1);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf1);
zf1.update();
@@ -279,7 +275,7 @@ public class JarSigningTest {
try (ZFile zf2 = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
me.register(zf2);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
zf2.add(file1Name, new ByteArrayInputStream(file1Contents));
@@ -312,7 +308,7 @@ public class JarSigningTest {
try (ZFile zf = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
zf.add(fileName, new ByteArrayInputStream(fileContents));
}
@@ -327,7 +323,7 @@ public class JarSigningTest {
try (ZFile zf = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
}
/*
@@ -364,7 +360,7 @@ public class JarSigningTest {
try (ZFile zf = new ZFile(zipFile)) {
ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
me.register(zf);
- new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
}
/*