From fa1da6c3114d9f0c0cd0e27025c07f73f1810b76 Mon Sep 17 00:00:00 2001 From: Alex Klyubin Date: Fri, 27 May 2016 14:36:05 -0700 Subject: [PATCH] Switch signapk to apksigner-core. This switches signapk's APK signing from its own signing logic to that offered by apksigner-core library. OTA update package signing logic remains inside signapk codebase. Bug: 27461702 Change-Id: Ibf8435c555fe3f2b621d5189e7ae44f79082c810 --- .../android/apksigner/core/apk/ApkUtils.java | 158 ++++ .../apksigner/core/internal/zip/ZipUtils.java | 225 +++++- .../core/zip/ZipFormatException.java | 32 + tools/signapk/Android.mk | 6 +- .../src/com/android/signapk/ApkSignerV2.java | 725 ------------------ .../signapk/src/com/android/signapk/Pair.java | 81 -- .../src/com/android/signapk/SignApk.java | 646 ++++++---------- .../src/com/android/signapk/ZipUtils.java | 162 ---- 8 files changed, 671 insertions(+), 1364 deletions(-) create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java create mode 100644 tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java delete mode 100644 tools/signapk/src/com/android/signapk/ApkSignerV2.java delete mode 100644 tools/signapk/src/com/android/signapk/Pair.java delete mode 100644 tools/signapk/src/com/android/signapk/ZipUtils.java diff --git a/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java new file mode 100644 index 0000000000..8cc8c90d23 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.apk; + +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.internal.zip.ZipUtils; +import com.android.apksigner.core.util.DataSource; +import com.android.apksigner.core.zip.ZipFormatException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * APK utilities. + */ +public class ApkUtils { + + private ApkUtils() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + Pair eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new ZipFormatException("ZIP End of Central Directory record not found"); + } + + ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); + long eocdOffset = eocdAndOffsetInFile.getSecond(); + if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { + throw new ZipFormatException("ZIP64 APK not supported"); + } + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); + if (cdStartOffset >= eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + + long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); + long cdEndOffset = cdStartOffset + cdSizeBytes; + if (cdEndOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset); + } + + int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); + + return new ZipSections( + cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf); + } + + /** + * Information about the ZIP sections of an APK. + */ + public static class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } + } + + /** + * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central + * Directory record. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must + * be between {@code 0} and {@code 2^32 - 1} inclusive. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java index 7b47e50e70..51110b62b6 100644 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java @@ -16,9 +16,14 @@ package com.android.apksigner.core.internal.zip; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import com.android.apksigner.core.internal.util.ByteBufferSink; +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.util.DataSource; + /** * Assorted ZIP format helpers. * @@ -28,7 +33,20 @@ import java.nio.ByteOrder; public abstract class ZipUtils { private ZipUtils() {} + public static final short COMPRESSION_METHOD_STORED = 0; + public static final short COMPRESSION_METHOD_DEFLATED = 8; + + private static final int ZIP_EOCD_REC_MIN_SIZE = 22; + private static final int ZIP_EOCD_REC_SIG = 0x06054b50; + private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; + private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; + + private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; + private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; + + private static final int UINT16_MAX_VALUE = 0xffff; /** * Sets the offset of the start of the ZIP Central Directory in the archive. @@ -44,16 +62,221 @@ public abstract class ZipUtils { offset); } + /** + * Returns the offset of the start of the ZIP Central Directory in the archive. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); + } + + /** + * Returns the total number of records in ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static int getZipEocdCentralDirectoryTotalRecordCount( + ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + public static Pair findZipEndOfCentralDirectoryRecord(DataSource zip) + throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + return null; + } + + // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus + // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily + // reading more data. + Pair result = findZipEndOfCentralDirectoryRecord(zip, 0); + if (result != null) { + return result; + } + + // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment + // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because + // the comment length field is an unsigned 16-bit number. + return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted + * value is from 0 to 65535 inclusive. The smaller the value, the faster this method + * locates the record, provided its comment field is no longer than this value. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + private static Pair findZipEndOfCentralDirectoryRecord( + DataSource zip, int maxCommentSize) throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { + throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); + } + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + // No space for EoCD record in the file. + return null; + } + // Lower maxCommentSize if the file is too small. + maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); + + ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + long bufOffsetInFile = fileSize - buf.capacity(); + zip.feed(bufOffsetInFile, buf.remaining(), new ByteBufferSink(buf)); + buf.flip(); + int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); + if (eocdOffsetInBuf == -1) { + // No EoCD record found in the buffer + return null; + } + // EoCD found + buf.position(eocdOffsetInBuf); + ByteBuffer eocd = buf.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); + } + + /** + * Returns the position at which ZIP End of Central Directory record starts in the provided + * buffer or {@code -1} if the record is not present. + * + *

NOTE: Byte order of {@code zipContents} must be little-endian. + */ + private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { + assertByteOrderLittleEndian(zipContents); + + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + int archiveSize = zipContents.capacity(); + if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { + return -1; + } + int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); + int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; + for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; + expectedCommentLength++) { + int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; + if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { + int actualCommentLength = + getUnsignedInt16( + zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); + if (actualCommentLength == expectedCommentLength) { + return eocdStartPos; + } + } + } + + return -1; + } + + /** + * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory + * Locator. + * + * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record + * in the file. + * + * @throws IOException if an I/O error occurs while reading the data source + */ + public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( + DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException { + + // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central + // Directory Record. + long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; + if (locatorPosition < 0) { + return false; + } + + ByteBuffer sig = ByteBuffer.allocate(4); + sig.order(ByteOrder.LITTLE_ENDIAN); + zip.feed(locatorPosition, sig.remaining(), new ByteBufferSink(sig)); + sig.flip(); + return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG; + } + private static void assertByteOrderLittleEndian(ByteBuffer buffer) { if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); } } + private static int getUnsignedInt16(ByteBuffer buffer, int offset) { + return buffer.getShort(offset) & 0xffff; + } + private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { if ((value < 0) || (value > 0xffffffffL)) { throw new IllegalArgumentException("uint32 value of out range: " + value); } - buffer.putInt(buffer.position() + offset, (int) value); + buffer.putInt(offset, (int) value); + } + + private static long getUnsignedInt32(ByteBuffer buffer, int offset) { + return buffer.getInt(offset) & 0xffffffffL; } } \ No newline at end of file diff --git a/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java new file mode 100644 index 0000000000..7da57d9f13 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.zip; + +/** + * Indicates that a ZIP archive is not well-formed. + */ +public class ZipFormatException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipFormatException(String message) { + super(message); + } + + public ZipFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/tools/signapk/Android.mk b/tools/signapk/Android.mk index ac217c7e48..eff066ccd1 100644 --- a/tools/signapk/Android.mk +++ b/tools/signapk/Android.mk @@ -21,7 +21,11 @@ include $(CLEAR_VARS) LOCAL_MODULE := signapk LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_JAR_MANIFEST := SignApk.mf -LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host bouncycastle-bcpkix-host conscrypt-host +LOCAL_STATIC_JAVA_LIBRARIES := \ + apksigner-core \ + bouncycastle-host \ + bouncycastle-bcpkix-host \ + conscrypt-host LOCAL_REQUIRED_MODULES := libconscrypt_openjdk_jni include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/signapk/src/com/android/signapk/ApkSignerV2.java b/tools/signapk/src/com/android/signapk/ApkSignerV2.java deleted file mode 100644 index 7b617dbe03..0000000000 --- a/tools/signapk/src/com/android/signapk/ApkSignerV2.java +++ /dev/null @@ -1,725 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.signapk; - -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.DigestException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * APK Signature Scheme v2 signer. - * - *

APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single - * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and - * uncompressed contents of ZIP entries. - */ -public abstract class ApkSignerV2 { - /* - * The two main goals of APK Signature Scheme v2 are: - * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature - * cover every byte of the APK being signed. - * 2. Enable much faster signature and integrity verification. This is achieved by requiring - * only a minimal amount of APK parsing before the signature is verified, thus completely - * bypassing ZIP entry decompression and by making integrity verification parallelizable by - * employing a hash tree. - * - * The generated signature block is wrapped into an APK Signing Block and inserted into the - * original APK immediately before the start of ZIP Central Directory. This is to ensure that - * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for - * extensibility. For example, a future signature scheme could insert its signatures there as - * well. The contract of the APK Signing Block is that all contents outside of the block must be - * protected by signatures inside the block. - */ - - public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; - public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; - public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; - public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; - public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; - public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; - public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; - - /** - * {@code .SF} file header section attribute indicating that the APK is signed not just with - * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute - * facilitates v2 signature stripping detection. - * - *

The attribute contains a comma-separated set of signature scheme IDs. - */ - public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed"; - public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2"; - - private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0; - private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1; - - private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; - - private static final byte[] APK_SIGNING_BLOCK_MAGIC = - new byte[] { - 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, - }; - private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; - - private ApkSignerV2() {} - - /** - * Signer configuration. - */ - public static final class SignerConfig { - /** Private key. */ - public PrivateKey privateKey; - - /** - * Certificates, with the first certificate containing the public key corresponding to - * {@link #privateKey}. - */ - public List certificates; - - /** - * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants). - */ - public List signatureAlgorithms; - } - - /** - * Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of - * consecutive chunks. - * - *

NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections - * of META-INF/*.SF files of APK being signed must contain the - * {@code X-Android-APK-Signed: true} attribute. - * - * @param inputApk contents of the APK to be signed. The APK starts at the current position - * of the buffer and ends at the limit of the buffer. - * @param signerConfigs signer configurations, one for each signer. - * - * @throws ApkParseException if the APK cannot be parsed. - * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or - * cannot be used in general. - * @throws SignatureException if an error occurs when computing digests of generating - * signatures. - */ - public static ByteBuffer[] sign( - ByteBuffer inputApk, - List signerConfigs) - throws ApkParseException, InvalidKeyException, SignatureException { - // Slice/create a view in the inputApk to make sure that: - // 1. inputApk is what's between position and limit of the original inputApk, and - // 2. changes to position, limit, and byte order are not reflected in the original. - ByteBuffer originalInputApk = inputApk; - inputApk = originalInputApk.slice(); - inputApk.order(ByteOrder.LITTLE_ENDIAN); - - // Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central - // Directory is immediately followed by the ZIP End of Central Directory. - int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk); - if (eocdOffset == -1) { - throw new ApkParseException("Failed to locate ZIP End of Central Directory"); - } - if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) { - throw new ApkParseException("ZIP64 format not supported"); - } - inputApk.position(eocdOffset); - long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk); - if (centralDirSizeLong > Integer.MAX_VALUE) { - throw new ApkParseException( - "ZIP Central Directory size out of range: " + centralDirSizeLong); - } - int centralDirSize = (int) centralDirSizeLong; - long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk); - if (centralDirOffsetLong > Integer.MAX_VALUE) { - throw new ApkParseException( - "ZIP Central Directory offset in file out of range: " + centralDirOffsetLong); - } - int centralDirOffset = (int) centralDirOffsetLong; - int expectedEocdOffset = centralDirOffset + centralDirSize; - if (expectedEocdOffset < centralDirOffset) { - throw new ApkParseException( - "ZIP Central Directory extent too large. Offset: " + centralDirOffset - + ", size: " + centralDirSize); - } - if (eocdOffset != expectedEocdOffset) { - throw new ApkParseException( - "ZIP Central Directory not immeiately followed by ZIP End of" - + " Central Directory. CD end: " + expectedEocdOffset - + ", EoCD start: " + eocdOffset); - } - - // Create ByteBuffers holding the contents of everything before ZIP Central Directory, - // ZIP Central Directory, and ZIP End of Central Directory. - inputApk.clear(); - ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset); - ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset); - // Create a copy of End of Central Directory because we'll need modify its contents later. - byte[] eocdBytes = new byte[inputApk.remaining()]; - inputApk.get(eocdBytes); - ByteBuffer eocd = ByteBuffer.wrap(eocdBytes); - eocd.order(inputApk.order()); - - // Figure which which digests to use for APK contents. - Set contentDigestAlgorithms = new HashSet<>(); - for (SignerConfig signerConfig : signerConfigs) { - for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { - contentDigestAlgorithms.add( - getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm)); - } - } - - // Compute digests of APK contents. - Map contentDigests; // digest algorithm ID -> digest - try { - contentDigests = - computeContentDigests( - contentDigestAlgorithms, - new ByteBuffer[] {beforeCentralDir, centralDir, eocd}); - } catch (DigestException e) { - throw new SignatureException("Failed to compute digests of APK", e); - } - - // Sign the digests and wrap the signatures and signer info into an APK Signing Block. - ByteBuffer apkSigningBlock = - ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests)); - - // Update Central Directory Offset in End of Central Directory Record. Central Directory - // follows the APK Signing Block and thus is shifted by the size of the APK Signing Block. - centralDirOffset += apkSigningBlock.remaining(); - eocd.clear(); - ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset); - - // Follow the Java NIO pattern for ByteBuffer whose contents have been consumed. - originalInputApk.position(originalInputApk.limit()); - - // Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the - // Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller. - // Contrary to the name, this does not clear the contents of these ByteBuffer. - beforeCentralDir.clear(); - centralDir.clear(); - eocd.clear(); - - // Insert APK Signing Block immediately before the ZIP Central Directory. - return new ByteBuffer[] { - beforeCentralDir, - apkSigningBlock, - centralDir, - eocd, - }; - } - - private static Map computeContentDigests( - Set digestAlgorithms, - ByteBuffer[] contents) throws DigestException { - // For each digest algorithm the result is computed as follows: - // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. - // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. - // No chunks are produced for empty (zero length) segments. - // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's - // length in bytes (uint32 little-endian) and the chunk's contents. - // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of - // chunks (uint32 little-endian) and the concatenation of digests of chunks of all - // segments in-order. - - int chunkCount = 0; - for (ByteBuffer input : contents) { - chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); - } - - final Map digestsOfChunks = new HashMap<>(digestAlgorithms.size()); - for (int digestAlgorithm : digestAlgorithms) { - int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); - byte[] concatenationOfChunkCountAndChunkDigests = - new byte[5 + chunkCount * digestOutputSizeBytes]; - concatenationOfChunkCountAndChunkDigests[0] = 0x5a; - setUnsignedInt32LittleEngian( - chunkCount, concatenationOfChunkCountAndChunkDigests, 1); - digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests); - } - - int chunkIndex = 0; - byte[] chunkContentPrefix = new byte[5]; - chunkContentPrefix[0] = (byte) 0xa5; - // Optimization opportunity: digests of chunks can be computed in parallel. - for (ByteBuffer input : contents) { - while (input.hasRemaining()) { - int chunkSize = - Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); - final ByteBuffer chunk = getByteBuffer(input, chunkSize); - for (int digestAlgorithm : digestAlgorithms) { - String jcaAlgorithmName = - getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); - MessageDigest md; - try { - md = MessageDigest.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new DigestException( - jcaAlgorithmName + " MessageDigest not supported", e); - } - // Reset position to 0 and limit to capacity. Position would've been modified - // by the preceding iteration of this loop. NOTE: Contrary to the method name, - // this does not modify the contents of the chunk. - chunk.clear(); - setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1); - md.update(chunkContentPrefix); - md.update(chunk); - byte[] concatenationOfChunkCountAndChunkDigests = - digestsOfChunks.get(digestAlgorithm); - int expectedDigestSizeBytes = - getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); - int actualDigestSizeBytes = - md.digest( - concatenationOfChunkCountAndChunkDigests, - 5 + chunkIndex * expectedDigestSizeBytes, - expectedDigestSizeBytes); - if (actualDigestSizeBytes != expectedDigestSizeBytes) { - throw new DigestException( - "Unexpected output size of " + md.getAlgorithm() - + " digest: " + actualDigestSizeBytes); - } - } - chunkIndex++; - } - } - - Map result = new HashMap<>(digestAlgorithms.size()); - for (Map.Entry entry : digestsOfChunks.entrySet()) { - int digestAlgorithm = entry.getKey(); - byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue(); - String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); - MessageDigest md; - try { - md = MessageDigest.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e); - } - result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests)); - } - return result; - } - - private static final int getChunkCount(int inputSize, int chunkSize) { - return (inputSize + chunkSize - 1) / chunkSize; - } - - private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) { - result[offset] = (byte) (value & 0xff); - result[offset + 1] = (byte) ((value >> 8) & 0xff); - result[offset + 2] = (byte) ((value >> 16) & 0xff); - result[offset + 3] = (byte) ((value >> 24) & 0xff); - } - - private static byte[] generateApkSigningBlock( - List signerConfigs, - Map 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 signerConfigs, - Map contentDigests) throws InvalidKeyException, SignatureException { - // FORMAT: - // * length-prefixed sequence of length-prefixed signer blocks. - - List signerBlocks = new ArrayList<>(signerConfigs.size()); - int signerNumber = 0; - for (SignerConfig signerConfig : signerConfigs) { - signerNumber++; - byte[] signerBlock; - try { - signerBlock = generateSignerBlock(signerConfig, contentDigests); - } catch (InvalidKeyException e) { - throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); - } catch (SignatureException e) { - throw new SignatureException("Signer #" + signerNumber + " failed", e); - } - signerBlocks.add(signerBlock); - } - - return encodeAsSequenceOfLengthPrefixedElements( - new byte[][] { - encodeAsSequenceOfLengthPrefixedElements(signerBlocks), - }); - } - - private static byte[] generateSignerBlock( - SignerConfig signerConfig, - Map 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> digests = - new ArrayList<>(signerConfig.signatureAlgorithms.size()); - for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { - int contentDigestAlgorithm = - getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm); - byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); - if (contentDigest == null) { - throw new RuntimeException( - getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm) - + " content digest for " - + getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm) - + " not computed"); - } - digests.add(Pair.create(signatureAlgorithm, contentDigest)); - } - signedData.digests = digests; - - V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer(); - // FORMAT: - // * length-prefixed sequence of length-prefixed digests: - // * uint32: signature algorithm ID - // * length-prefixed bytes: digest of contents - // * length-prefixed sequence of certificates: - // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). - // * length-prefixed sequence of length-prefixed additional attributes: - // * uint32: ID - // * (length - 4) bytes: value - signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] { - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests), - encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), - // additional attributes - new byte[0], - }); - signer.publicKey = encodedPublicKey; - signer.signatures = new ArrayList<>(); - for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { - Pair signatureParams = - getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm); - String jcaSignatureAlgorithm = signatureParams.getFirst(); - AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond(); - byte[] signatureBytes; - try { - Signature signature = Signature.getInstance(jcaSignatureAlgorithm); - signature.initSign(signerConfig.privateKey); - if (jcaSignatureAlgorithmParams != null) { - signature.setParameter(jcaSignatureAlgorithmParams); - } - signature.update(signer.signedData); - signatureBytes = signature.sign(); - } catch (InvalidKeyException e) { - throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException - | SignatureException e) { - throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e); - } - - try { - Signature signature = Signature.getInstance(jcaSignatureAlgorithm); - signature.initVerify(publicKey); - if (jcaSignatureAlgorithmParams != null) { - signature.setParameter(jcaSignatureAlgorithmParams); - } - signature.update(signer.signedData); - if (!signature.verify(signatureBytes)) { - throw new SignatureException("Signature did not verify"); - } - } catch (InvalidKeyException e) { - throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm - + " signature using public key from certificate", e); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException - | SignatureException e) { - throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm - + " signature using public key from certificate", e); - } - - signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes)); - } - - // FORMAT: - // * length-prefixed signed data - // * length-prefixed sequence of length-prefixed signatures: - // * uint32: signature algorithm ID - // * length-prefixed bytes: signature of signed data - // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) - return encodeAsSequenceOfLengthPrefixedElements( - new byte[][] { - signer.signedData, - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( - signer.signatures), - signer.publicKey, - }); - } - - private static final class V2SignatureSchemeBlock { - private static final class Signer { - public byte[] signedData; - public List> signatures; - public byte[] publicKey; - } - - private static final class SignedData { - public List> digests; - public List certificates; - } - } - - private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException { - byte[] encodedPublicKey = null; - if ("X.509".equals(publicKey.getFormat())) { - encodedPublicKey = publicKey.getEncoded(); - } - if (encodedPublicKey == null) { - try { - encodedPublicKey = - KeyFactory.getInstance(publicKey.getAlgorithm()) - .getKeySpec(publicKey, X509EncodedKeySpec.class) - .getEncoded(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new InvalidKeyException( - "Failed to obtain X.509 encoded form of public key " + publicKey - + " of class " + publicKey.getClass().getName(), - e); - } - } - if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { - throw new InvalidKeyException( - "Failed to obtain X.509 encoded form of public key " + publicKey - + " of class " + publicKey.getClass().getName()); - } - return encodedPublicKey; - } - - public static List encodeCertificates(List certificates) - throws CertificateEncodingException { - List result = new ArrayList<>(); - for (X509Certificate certificate : certificates) { - result.add(certificate.getEncoded()); - } - return result; - } - - private static byte[] encodeAsSequenceOfLengthPrefixedElements(List 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> sequence) { - int resultSize = 0; - for (Pair element : sequence) { - resultSize += 12 + element.getSecond().length; - } - ByteBuffer result = ByteBuffer.allocate(resultSize); - result.order(ByteOrder.LITTLE_ENDIAN); - for (Pair element : sequence) { - byte[] second = element.getSecond(); - result.putInt(8 + second.length); - result.putInt(element.getFirst()); - result.putInt(second.length); - result.put(second); - } - return result.array(); - } - - /** - * Relative get method for reading {@code size} number of bytes from the current - * position of this buffer. - * - *

This method reads the next {@code size} bytes at this buffer's current position, - * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to - * {@code size}, byte order set to this buffer's byte order; and then increments the position by - * {@code size}. - */ - private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { - if (size < 0) { - throw new IllegalArgumentException("size: " + size); - } - int originalLimit = source.limit(); - int position = source.position(); - int limit = position + size; - if ((limit < position) || (limit > originalLimit)) { - throw new BufferUnderflowException(); - } - source.limit(limit); - try { - ByteBuffer result = source.slice(); - result.order(source.order()); - source.position(limit); - return result; - } finally { - source.limit(originalLimit); - } - } - - private static Pair - getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) { - switch (sigAlgorithm) { - case SIGNATURE_RSA_PSS_WITH_SHA256: - return Pair.create( - "SHA256withRSA/PSS", - new PSSParameterSpec( - "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); - case SIGNATURE_RSA_PSS_WITH_SHA512: - return Pair.create( - "SHA512withRSA/PSS", - new PSSParameterSpec( - "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: - return Pair.create("SHA256withRSA", null); - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: - return Pair.create("SHA512withRSA", null); - case SIGNATURE_ECDSA_WITH_SHA256: - return Pair.create("SHA256withECDSA", null); - case SIGNATURE_ECDSA_WITH_SHA512: - return Pair.create("SHA512withECDSA", null); - case SIGNATURE_DSA_WITH_SHA256: - return Pair.create("SHA256withDSA", null); - default: - throw new IllegalArgumentException( - "Unknown signature algorithm: 0x" - + Long.toHexString(sigAlgorithm & 0xffffffff)); - } - } - - private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) { - switch (sigAlgorithm) { - case SIGNATURE_RSA_PSS_WITH_SHA256: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: - case SIGNATURE_ECDSA_WITH_SHA256: - case SIGNATURE_DSA_WITH_SHA256: - return CONTENT_DIGEST_CHUNKED_SHA256; - case SIGNATURE_RSA_PSS_WITH_SHA512: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: - case SIGNATURE_ECDSA_WITH_SHA512: - return CONTENT_DIGEST_CHUNKED_SHA512; - default: - throw new IllegalArgumentException( - "Unknown signature algorithm: 0x" - + Long.toHexString(sigAlgorithm & 0xffffffff)); - } - } - - private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { - switch (digestAlgorithm) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return "SHA-256"; - case CONTENT_DIGEST_CHUNKED_SHA512: - return "SHA-512"; - default: - throw new IllegalArgumentException( - "Unknown content digest algorthm: " + digestAlgorithm); - } - } - - private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) { - switch (digestAlgorithm) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return 256 / 8; - case CONTENT_DIGEST_CHUNKED_SHA512: - return 512 / 8; - default: - throw new IllegalArgumentException( - "Unknown content digest algorthm: " + digestAlgorithm); - } - } - - /** - * Indicates that APK file could not be parsed. - */ - public static class ApkParseException extends Exception { - private static final long serialVersionUID = 1L; - - public ApkParseException(String message) { - super(message); - } - - public ApkParseException(String message, Throwable cause) { - super(message, cause); - } - } -} diff --git a/tools/signapk/src/com/android/signapk/Pair.java b/tools/signapk/src/com/android/signapk/Pair.java deleted file mode 100644 index e4a6c92251..0000000000 --- a/tools/signapk/src/com/android/signapk/Pair.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.signapk; - -/** - * Pair of two elements. - */ -public final class Pair { - private final A mFirst; - private final B mSecond; - - private Pair(A first, B second) { - mFirst = first; - mSecond = second; - } - - public static Pair create(A first, B second) { - return new Pair(first, second); - } - - public A getFirst() { - return mFirst; - } - - public B getSecond() { - return mSecond; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); - result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - @SuppressWarnings("rawtypes") - Pair other = (Pair) obj; - if (mFirst == null) { - if (other.mFirst != null) { - return false; - } - } else if (!mFirst.equals(other.mFirst)) { - return false; - } - if (mSecond == null) { - if (other.mSecond != null) { - return false; - } - } else if (!mSecond.equals(other.mSecond)) { - return false; - } - return true; - } -} diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java index a7c9fc3f30..d84d0700dc 100644 --- a/tools/signapk/src/com/android/signapk/SignApk.java +++ b/tools/signapk/src/com/android/signapk/SignApk.java @@ -23,7 +23,6 @@ import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.bouncycastle.cms.CMSTypedData; @@ -33,9 +32,15 @@ import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.util.encoders.Base64; import org.conscrypt.OpenSSLProvider; +import com.android.apksigner.core.ApkSignerEngine; +import com.android.apksigner.core.DefaultApkSignerEngine; +import com.android.apksigner.core.apk.ApkUtils; +import com.android.apksigner.core.util.DataSink; +import com.android.apksigner.core.util.DataSources; +import com.android.apksigner.core.zip.ZipFormatException; + import java.io.Console; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -49,19 +54,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.PrintStream; import java.lang.reflect.Constructor; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.DigestOutputStream; import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; -import java.security.MessageDigest; import java.security.PrivateKey; import java.security.Provider; -import java.security.PublicKey; import java.security.Security; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateFactory; @@ -71,17 +71,12 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; -import java.util.Iterator; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.TimeZone; -import java.util.TreeMap; -import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; import java.util.regex.Pattern; import javax.crypto.Cipher; @@ -113,11 +108,6 @@ import javax.crypto.spec.PBEKeySpec; * APK Signature Scheme v2. */ class SignApk { - private static final String CERT_SF_NAME = "META-INF/CERT.SF"; - private static final String CERT_SIG_NAME = "META-INF/CERT.%s"; - private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF"; - private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s"; - private static final String OTACERT_NAME = "META-INF/com/android/otacert"; /** @@ -137,36 +127,6 @@ class SignApk { private static final int USE_SHA1 = 1; private static final int USE_SHA256 = 2; - /** Digest algorithm used when signing the APK using APK Signature Scheme v2. */ - private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256"; - - /** - * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used - * for v1 signing (JAR signing) an APK using the private key corresponding to the provided - * certificate. - * - * @param minSdkVersion minimum Android platform API Level supported by the APK (see - * minSdkVersion attribute in AndroidManifest.xml). The higher the minSdkVersion, the - * stronger hash may be used for signing the APK. - */ - private static int getV1DigestAlgorithmForApk(X509Certificate cert, int minSdkVersion) { - String keyAlgorithm = cert.getPublicKey().getAlgorithm(); - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { - // RSA can be used only with SHA-1 prior to API Level 18. - return (minSdkVersion < 18) ? USE_SHA1 : USE_SHA256; - } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { - // ECDSA cannot be used prior to API Level 18 at all. It can only be used with SHA-1 - // on API Levels 18, 19, and 20. - if (minSdkVersion < 18) { - throw new IllegalArgumentException( - "ECDSA signatures only supported for minSdkVersion 18 and higher"); - } - return (minSdkVersion < 21) ? USE_SHA1 : USE_SHA256; - } else { - throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); - } - } - /** * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used * for signing an OTA update package using the private key corresponding to the provided @@ -187,10 +147,10 @@ class SignApk { /** * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA - * or v1 signing an APK using the private key corresponding to the provided certificate and the + * update package using the private key corresponding to the provided certificate and the * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). */ - private static String getJcaSignatureAlgorithmForV1SigningOrOta( + private static String getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash) { String sigAlgDigestPrefix; switch (hash) { @@ -214,11 +174,6 @@ class SignApk { } } - /* Files matching this pattern are not copied to the output. */ - private static final Pattern STRIP_PATTERN = - Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" - + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); - private static X509Certificate readPublicKey(File file) throws IOException, GeneralSecurityException { FileInputStream input = new FileInputStream(file); @@ -323,89 +278,6 @@ class SignApk { } } - /** - * Add the hash(es) of every file to the manifest, creating it if - * necessary. - */ - private static Manifest addDigestsToManifest( - JarFile jar, Pattern ignoredFilenamePattern, int hashes) - throws IOException, GeneralSecurityException { - Manifest input = jar.getManifest(); - Manifest output = new Manifest(); - Attributes main = output.getMainAttributes(); - if (input != null) { - main.putAll(input.getMainAttributes()); - } else { - main.putValue("Manifest-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android SignApk)"); - } - - MessageDigest md_sha1 = null; - MessageDigest md_sha256 = null; - if ((hashes & USE_SHA1) != 0) { - md_sha1 = MessageDigest.getInstance("SHA1"); - } - if ((hashes & USE_SHA256) != 0) { - md_sha256 = MessageDigest.getInstance("SHA256"); - } - - byte[] buffer = new byte[4096]; - int num; - - // We sort the input entries by name, and add them to the - // output manifest in sorted order. We expect that the output - // map will be deterministic. - - TreeMap byName = new TreeMap(); - - for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { - JarEntry entry = e.nextElement(); - byName.put(entry.getName(), entry); - } - - for (JarEntry entry: byName.values()) { - String name = entry.getName(); - if (!entry.isDirectory() - && (ignoredFilenamePattern == null - || !ignoredFilenamePattern.matcher(name).matches())) { - InputStream data = jar.getInputStream(entry); - while ((num = data.read(buffer)) > 0) { - if (md_sha1 != null) md_sha1.update(buffer, 0, num); - if (md_sha256 != null) md_sha256.update(buffer, 0, num); - } - - Attributes attr = null; - if (input != null) attr = input.getAttributes(name); - attr = attr != null ? new Attributes(attr) : new Attributes(); - // Remove any previously computed digests from this entry's attributes. - for (Iterator i = attr.keySet().iterator(); i.hasNext();) { - Object key = i.next(); - if (!(key instanceof Attributes.Name)) { - continue; - } - String attributeNameLowerCase = - ((Attributes.Name) key).toString().toLowerCase(Locale.US); - if (attributeNameLowerCase.endsWith("-digest")) { - i.remove(); - } - } - // Add SHA-1 digest if requested - if (md_sha1 != null) { - attr.putValue("SHA1-Digest", - new String(Base64.encode(md_sha1.digest()), "ASCII")); - } - // Add SHA-256 digest if requested - if (md_sha256 != null) { - attr.putValue("SHA-256-Digest", - new String(Base64.encode(md_sha256.digest()), "ASCII")); - } - output.getEntries().put(name, attr); - } - } - - return output; - } - /** * Add a copy of the public key to the archive; this should * exactly match one of the files in @@ -416,7 +288,7 @@ class SignApk { private static void addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp) - throws IOException, GeneralSecurityException { + throws IOException { JarEntry je = new JarEntry(OTACERT_NAME); je.setTime(timestamp); @@ -431,94 +303,6 @@ class SignApk { } - /** Write to another stream and track how many bytes have been - * written. - */ - private static class CountOutputStream extends FilterOutputStream { - private int mCount; - - public CountOutputStream(OutputStream out) { - super(out); - mCount = 0; - } - - @Override - public void write(int b) throws IOException { - super.write(b); - mCount++; - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - super.write(b, off, len); - mCount += len; - } - - public int size() { - return mCount; - } - } - - /** Write a .SF file with a digest of the specified manifest. */ - private static void writeSignatureFile(Manifest manifest, OutputStream out, - int hash, boolean additionallySignedUsingAnApkSignatureScheme) - throws IOException, GeneralSecurityException { - Manifest sf = new Manifest(); - Attributes main = sf.getMainAttributes(); - main.putValue("Signature-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android SignApk)"); - if (additionallySignedUsingAnApkSignatureScheme) { - // Add APK Signature Scheme v2 signature stripping protection. - // This attribute indicates that this APK is supposed to have been signed using one or - // more APK-specific signature schemes in addition to the standard JAR signature scheme - // used by this code. APK signature verifier should reject the APK if it does not - // contain a signature for the signature scheme the verifier prefers out of this set. - main.putValue( - ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME, - ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE); - } - - MessageDigest md = MessageDigest.getInstance( - hash == USE_SHA256 ? "SHA256" : "SHA1"); - PrintStream print = new PrintStream( - new DigestOutputStream(new ByteArrayOutputStream(), md), - true, "UTF-8"); - - // Digest of the entire manifest - manifest.write(print); - print.flush(); - main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", - new String(Base64.encode(md.digest()), "ASCII")); - - Map entries = manifest.getEntries(); - for (Map.Entry entry : entries.entrySet()) { - // Digest of the manifest stanza for this entry. - print.print("Name: " + entry.getKey() + "\r\n"); - for (Map.Entry att : entry.getValue().entrySet()) { - print.print(att.getKey() + ": " + att.getValue() + "\r\n"); - } - print.print("\r\n"); - print.flush(); - - Attributes sfAttr = new Attributes(); - sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest", - new String(Base64.encode(md.digest()), "ASCII")); - sf.getEntries().put(entry.getKey(), sfAttr); - } - - CountOutputStream cout = new CountOutputStream(out); - sf.write(cout); - - // A bug in the java.util.jar implementation of Android platforms - // up to version 1.6 will cause a spurious IOException to be thrown - // if the length of the signature file is a multiple of 1024 bytes. - // As a workaround, add an extra CRLF in this case. - if ((cout.size() % 1024) == 0) { - cout.write('\r'); - cout.write('\n'); - } - } - /** Sign data and write the digital signature to 'out'. */ private static void writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, @@ -534,7 +318,7 @@ class SignApk { CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); ContentSigner signer = new JcaContentSignerBuilder( - getJcaSignatureAlgorithmForV1SigningOrOta(publicKey, hash)) + getJcaSignatureAlgorithmForOta(publicKey, hash)) .build(privateKey); gen.addSignerInfoGenerator( new JcaSignerInfoGeneratorBuilder( @@ -551,13 +335,40 @@ class SignApk { } } + /** + * Adds ZIP entries which represent the v1 signature (JAR signature scheme). + */ + private static void addV1Signature( + ApkSignerEngine apkSigner, + ApkSignerEngine.OutputJarSignatureRequest v1Signature, + JarOutputStream out, + long timestamp) throws IOException { + for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry + : v1Signature.getAdditionalJarEntries()) { + String entryName = entry.getName(); + JarEntry outEntry = new JarEntry(entryName); + outEntry.setTime(timestamp); + out.putNextEntry(outEntry); + byte[] entryData = entry.getData(); + out.write(entryData); + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + apkSigner.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); + inspectEntryRequest.done(); + } + } + } + /** * Copy all JAR entries from input to output. We set the modification times in the output to a * fixed time, so as to reduce variation in the output file and make incremental OTAs more * efficient. */ - private static void copyFiles(JarFile in, + private static void copyFiles( + JarFile in, Pattern ignoredFilenamePattern, + ApkSignerEngine apkSigner, JarOutputStream out, long timestamp, int defaultAlignment) throws IOException { @@ -589,12 +400,21 @@ class SignApk { // the start of the file and makes it easier to do alignment // on them (since only stored entries are aligned). + List remainingNames = new ArrayList<>(names.size()); for (String name : names) { JarEntry inEntry = in.getJarEntry(name); - JarEntry outEntry = null; - if (inEntry.getMethod() != JarEntry.STORED) continue; + if (inEntry.getMethod() != JarEntry.STORED) { + // Defer outputting this entry until we're ready to output compressed entries. + remainingNames.add(name); + continue; + } + + if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { + continue; + } + // Preserve the STORED method of the input entry. - outEntry = new JarEntry(inEntry); + JarEntry outEntry = new JarEntry(inEntry); outEntry.setTime(timestamp); // Discard comment and extra fields of this entry to // simplify alignment logic below and for consistency with @@ -638,33 +458,97 @@ class SignApk { offset += extra.length; out.putNextEntry(outEntry); + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; + DataSink entryDataSink = + (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; - InputStream data = in.getInputStream(inEntry); - while ((num = data.read(buffer)) > 0) { - out.write(buffer, 0, num); - offset += num; + try (InputStream data = in.getInputStream(inEntry)) { + while ((num = data.read(buffer)) > 0) { + out.write(buffer, 0, num); + if (entryDataSink != null) { + entryDataSink.consume(buffer, 0, num); + } + offset += num; + } } out.flush(); + if (inspectEntryRequest != null) { + inspectEntryRequest.done(); + } } // Copy all the non-STORED entries. We don't attempt to // maintain the 'offset' variable past this point; we don't do // alignment on these entries. - for (String name : names) { + for (String name : remainingNames) { JarEntry inEntry = in.getJarEntry(name); - JarEntry outEntry = null; - if (inEntry.getMethod() == JarEntry.STORED) continue; + if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { + continue; + } + // Create a new entry so that the compressed len is recomputed. - outEntry = new JarEntry(name); + JarEntry outEntry = new JarEntry(name); outEntry.setTime(timestamp); out.putNextEntry(outEntry); + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; + DataSink entryDataSink = + (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; InputStream data = in.getInputStream(inEntry); while ((num = data.read(buffer)) > 0) { out.write(buffer, 0, num); + if (entryDataSink != null) { + entryDataSink.consume(buffer, 0, num); + } } out.flush(); + if (inspectEntryRequest != null) { + inspectEntryRequest.done(); + } + } + } + + private static boolean shouldOutputApkEntry( + ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) + throws IOException { + if (apkSigner == null) { + return true; + } + + ApkSignerEngine.InputJarEntryInstructions instructions = + apkSigner.inputJarEntry(inEntry.getName()); + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + instructions.getInspectJarEntryRequest(); + if (inspectEntryRequest != null) { + provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); + } + switch (instructions.getOutputPolicy()) { + case OUTPUT: + return true; + case SKIP: + case OUTPUT_BY_ENGINE: + return false; + default: + throw new RuntimeException( + "Unsupported output policy: " + instructions.getOutputPolicy()); + } + } + + private static void provideJarEntry( + JarFile jarFile, + JarEntry jarEntry, + ApkSignerEngine.InspectJarEntryRequest request, + byte[] tmpbuf) throws IOException { + DataSink dataSink = request.getDataSink(); + try (InputStream in = jarFile.getInputStream(jarEntry)) { + int chunkSize; + while ((chunkSize = in.read(tmpbuf)) > 0) { + dataSink.consume(tmpbuf, 0, chunkSize); + } + request.done(); } } @@ -756,6 +640,11 @@ class SignApk { private final ASN1ObjectIdentifier type; private WholeFileSignerOutputStream signer; + // Files matching this pattern are not copied to the output. + private static final Pattern STRIP_PATTERN = + Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); + public CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream) { @@ -789,7 +678,7 @@ class SignApk { signer = new WholeFileSignerOutputStream(out, outputStream); JarOutputStream outputJar = new JarOutputStream(signer); - copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, 0); + copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0); addOtacert(outputJar, publicKeyFile, timestamp); signer.notifyClosing(); @@ -883,47 +772,6 @@ class SignApk { temp.writeTo(outputStream); } - private static void signFile(Manifest manifest, - X509Certificate[] publicKey, PrivateKey[] privateKey, int[] hash, - long timestamp, - boolean additionallySignedUsingAnApkSignatureScheme, - JarOutputStream outputJar) - throws Exception { - - // MANIFEST.MF - JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); - je.setTime(timestamp); - outputJar.putNextEntry(je); - manifest.write(outputJar); - - int numKeys = publicKey.length; - for (int k = 0; k < numKeys; ++k) { - // CERT.SF / CERT#.SF - je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : - (String.format(CERT_SF_MULTI_NAME, k))); - je.setTime(timestamp); - outputJar.putNextEntry(je); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeSignatureFile( - manifest, - baos, - hash[k], - additionallySignedUsingAnApkSignatureScheme); - byte[] signedData = baos.toByteArray(); - outputJar.write(signedData); - - // CERT.{EC,RSA} / CERT#.{EC,RSA} - final String keyType = publicKey[k].getPublicKey().getAlgorithm(); - je = new JarEntry(numKeys == 1 ? - (String.format(CERT_SIG_NAME, keyType)) : - (String.format(CERT_SIG_MULTI_NAME, k, keyType))); - je.setTime(timestamp); - outputJar.putNextEntry(je); - writeSignatureBlock(new CMSProcessableByteArray(signedData), - publicKey[k], privateKey[k], hash[k], outputJar); - } - } - /** * Tries to load a JSE Provider by class name. This is for custom PrivateKey * types that might be stored in PKCS#11-like storage. @@ -976,81 +824,68 @@ class SignApk { Security.insertProviderAt((Provider) o, 1); } - /** - * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms - * into a list of APK Signature Scheme v2 {@code SignerConfig} instances. - */ - public static List createV2SignerConfigs( - PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms) - throws InvalidKeyException { + private static List createSignerConfigs( + PrivateKey[] privateKeys, X509Certificate[] certificates) { if (privateKeys.length != certificates.length) { throw new IllegalArgumentException( "The number of private keys must match the number of certificates: " + privateKeys.length + " vs" + certificates.length); } - List result = new ArrayList<>(privateKeys.length); + List signerConfigs = new ArrayList<>(); + String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; for (int i = 0; i < privateKeys.length; i++) { - PrivateKey privateKey = privateKeys[i]; - X509Certificate certificate = certificates[i]; - PublicKey publicKey = certificate.getPublicKey(); - String keyAlgorithm = privateKey.getAlgorithm(); - if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) { - throw new InvalidKeyException( - "Key algorithm of private key #" + (i + 1) + " does not match key" - + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm - + " vs " + publicKey.getAlgorithm()); - } - ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig(); - signerConfig.privateKey = privateKey; - signerConfig.certificates = Collections.singletonList(certificate); - List signatureAlgorithms = new ArrayList<>(digestAlgorithms.length); - for (String digestAlgorithm : digestAlgorithms) { - try { - signatureAlgorithms.add( - getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm)); - } catch (IllegalArgumentException e) { - throw new InvalidKeyException( - "Unsupported key and digest algorithm combination for signer #" - + (i + 1), - e); - } - } - signerConfig.signatureAlgorithms = signatureAlgorithms; - result.add(signerConfig); + String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); + DefaultApkSignerEngine.SignerConfig signerConfig = + new DefaultApkSignerEngine.SignerConfig.Builder( + signerName, + privateKeys[i], + Collections.singletonList(certificates[i])) + .build(); + signerConfigs.add(signerConfig); } - return result; + return signerConfigs; } - private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) { - if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) { - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { - // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee - // deterministic signatures which make life easier for OTA updates (fewer files - // changed when deterministic signature schemes are used). - return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256; - } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { - return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256; - } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { - return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256; - } else { - throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); - } - } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) { - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { - // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee - // deterministic signatures which make life easier for OTA updates (fewer files - // changed when deterministic signature schemes are used). - return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512; - } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { - return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512; - } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { - throw new IllegalArgumentException("SHA-512 is not supported with DSA"); - } else { - throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); - } - } else { - throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm); + private static class ZipSections { + ByteBuffer beforeCentralDir; + ByteBuffer centralDir; + ByteBuffer eocd; + } + + private static ZipSections findMainZipSections(ByteBuffer apk) + throws IOException, ZipFormatException { + apk.slice(); + ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk)); + long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); + long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); + long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; + long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new ZipFormatException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); } + apk.position(0); + apk.limit((int) centralDirStartOffset); + ByteBuffer beforeCentralDir = apk.slice(); + + apk.position((int) centralDirStartOffset); + apk.limit((int) centralDirEndOffset); + ByteBuffer centralDir = apk.slice(); + + apk.position((int) eocdStartOffset); + apk.limit(apk.capacity()); + ByteBuffer eocd = apk.slice(); + + apk.position(0); + apk.limit(apk.capacity()); + + ZipSections result = new ZipSections(); + result.beforeCentralDir = beforeCentralDir; + result.centralDir = centralDir; + result.eocd = eocd; + return result; } private static void usage() { @@ -1167,57 +1002,80 @@ class SignApk { timestamp, outputFile); } else { - // Generate, in memory, an APK signed using standard JAR Signature Scheme. - ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); - JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); - // Use maximum compression for compressed entries because the APK lives forever on - // the system partition. - outputJar.setLevel(9); - int v1DigestAlgorithmBitSet = 0; - int[] v1DigestAlgorithm = new int[numKeys]; - for (int i = 0; i < numKeys; ++i) { - v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion); - v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i]; - } - Manifest manifest = - addDigestsToManifest(inputJar, STRIP_PATTERN, v1DigestAlgorithmBitSet); - copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, alignment); - signFile( - manifest, - publicKey, privateKey, v1DigestAlgorithm, - timestamp, signUsingApkSignatureSchemeV2, - outputJar); - outputJar.close(); - ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); - v1SignedApkBuf.reset(); + try (ApkSignerEngine apkSigner = + new DefaultApkSignerEngine.Builder( + createSignerConfigs(privateKey, publicKey), minSdkVersion) + .setV1SigningEnabled(true) + .setV2SigningEnabled(signUsingApkSignatureSchemeV2) + .setOtherSignersSignaturesPreserved(false) + .build()) { + // We don't preserve the input APK's APK Signing Block (which contains v2 + // signatures) + apkSigner.inputApkSigningBlock(null); - ByteBuffer[] outputChunks; - if (signUsingApkSignatureSchemeV2) { - // Additionally sign the APK using the APK Signature Scheme v2. - ByteBuffer apkContents = v1SignedApk; - List signerConfigs = - createV2SignerConfigs( - privateKey, - publicKey, - new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM}); - outputChunks = ApkSignerV2.sign(apkContents, signerConfigs); - } else { - // Output the JAR-signed APK as is. - outputChunks = new ByteBuffer[] {v1SignedApk}; + // Build the output APK in memory, by copying input APK's ZIP entries across + // and then signing the output APK. + ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); + JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); + // Use maximum compression for compressed entries because the APK lives forever + // on the system partition. + outputJar.setLevel(9); + copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment); + ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = + apkSigner.outputJarEntries(); + if (addV1SignatureRequest != null) { + addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); + addV1SignatureRequest.done(); + } + outputJar.close(); + ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); + v1SignedApkBuf.reset(); + ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk}; + + ZipSections zipSections = findMainZipSections(v1SignedApk); + ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest = + apkSigner.outputZipSections( + DataSources.asDataSource(zipSections.beforeCentralDir), + DataSources.asDataSource(zipSections.centralDir), + DataSources.asDataSource(zipSections.eocd)); + if (addV2SignatureRequest != null) { + // Need to insert the returned APK Signing Block before ZIP Central + // Directory. + byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); + // Because the APK Signing Block is inserted before the Central Directory, + // we need to adjust accordingly the offset of Central Directory inside the + // ZIP End of Central Directory (EoCD) record. + ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining()); + modifiedEocd.put(zipSections.eocd); + modifiedEocd.flip(); + modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); + ApkUtils.setZipEocdCentralDirectoryOffset( + modifiedEocd, + zipSections.beforeCentralDir.remaining() + apkSigningBlock.length); + outputChunks = + new ByteBuffer[] { + zipSections.beforeCentralDir, + ByteBuffer.wrap(apkSigningBlock), + zipSections.centralDir, + modifiedEocd}; + addV2SignatureRequest.done(); + } + + // This assumes outputChunks are array-backed. To avoid this assumption, the + // code could be rewritten to use FileChannel. + for (ByteBuffer outputChunk : outputChunks) { + outputFile.write( + outputChunk.array(), + outputChunk.arrayOffset() + outputChunk.position(), + outputChunk.remaining()); + outputChunk.position(outputChunk.limit()); + } + + outputFile.close(); + outputFile = null; + apkSigner.outputDone(); } - // This assumes outputChunks are array-backed. To avoid this assumption, the - // code could be rewritten to use FileChannel. - for (ByteBuffer outputChunk : outputChunks) { - outputFile.write( - outputChunk.array(), - outputChunk.arrayOffset() + outputChunk.position(), - outputChunk.remaining()); - outputChunk.position(outputChunk.limit()); - } - - outputFile.close(); - outputFile = null; return; } } catch (Exception e) { diff --git a/tools/signapk/src/com/android/signapk/ZipUtils.java b/tools/signapk/src/com/android/signapk/ZipUtils.java deleted file mode 100644 index 7575a77d9e..0000000000 --- a/tools/signapk/src/com/android/signapk/ZipUtils.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.signapk; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Assorted ZIP format helpers. - * - *

NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte - * order of these buffers is little-endian. - */ -public abstract class ZipUtils { - private ZipUtils() {} - - private static final int ZIP_EOCD_REC_MIN_SIZE = 22; - private static final int ZIP_EOCD_REC_SIG = 0x06054b50; - private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; - private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; - private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; - - private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; - private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; - - private static final int UINT16_MAX_VALUE = 0xffff; - - /** - * Returns the position at which ZIP End of Central Directory record starts in the provided - * buffer or {@code -1} if the record is not present. - * - *

NOTE: Byte order of {@code zipContents} must be little-endian. - */ - public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { - assertByteOrderLittleEndian(zipContents); - - // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. - // The record can be identified by its 4-byte signature/magic which is located at the very - // beginning of the record. A complication is that the record is variable-length because of - // the comment field. - // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from - // end of the buffer for the EOCD record signature. Whenever we find a signature, we check - // the candidate record's comment length is such that the remainder of the record takes up - // exactly the remaining bytes in the buffer. The search is bounded because the maximum - // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. - - int archiveSize = zipContents.capacity(); - if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { - return -1; - } - int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); - int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; - for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; - expectedCommentLength++) { - int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; - if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { - int actualCommentLength = - getUnsignedInt16( - zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); - if (actualCommentLength == expectedCommentLength) { - return eocdStartPos; - } - } - } - - return -1; - } - - /** - * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory - * Locator. - * - *

NOTE: Byte order of {@code zipContents} must be little-endian. - */ - public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( - ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) { - assertByteOrderLittleEndian(zipContents); - - // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central - // Directory Record. - - int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; - if (locatorPosition < 0) { - return false; - } - - return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG; - } - - /** - * Returns the offset of the start of the ZIP Central Directory in the archive. - * - *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. - */ - public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { - assertByteOrderLittleEndian(zipEndOfCentralDirectory); - return getUnsignedInt32( - zipEndOfCentralDirectory, - zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); - } - - /** - * Sets the offset of the start of the ZIP Central Directory in the archive. - * - *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. - */ - public static void setZipEocdCentralDirectoryOffset( - ByteBuffer zipEndOfCentralDirectory, long offset) { - assertByteOrderLittleEndian(zipEndOfCentralDirectory); - setUnsignedInt32( - zipEndOfCentralDirectory, - zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, - offset); - } - - /** - * Returns the size (in bytes) of the ZIP Central Directory. - * - *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. - */ - public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { - assertByteOrderLittleEndian(zipEndOfCentralDirectory); - return getUnsignedInt32( - zipEndOfCentralDirectory, - zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); - } - - private static void assertByteOrderLittleEndian(ByteBuffer buffer) { - if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { - throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); - } - } - - private static int getUnsignedInt16(ByteBuffer buffer, int offset) { - return buffer.getShort(offset) & 0xffff; - } - - private static long getUnsignedInt32(ByteBuffer buffer, int offset) { - return buffer.getInt(offset) & 0xffffffffL; - } - - private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { - if ((value < 0) || (value > 0xffffffffL)) { - throw new IllegalArgumentException("uint32 value of out range: " + value); - } - buffer.putInt(buffer.position() + offset, (int) value); - } -}