1561 lines
69 KiB
Java
1561 lines
69 KiB
Java
/*
|
|
* 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.apksig;
|
|
|
|
import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
|
|
|
import com.android.apksig.apk.ApkFormatException;
|
|
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
|
|
import com.android.apksig.apk.ApkUtils;
|
|
import com.android.apksig.apk.MinSdkVersionException;
|
|
import com.android.apksig.internal.util.ByteBufferDataSource;
|
|
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
|
import com.android.apksig.internal.zip.EocdRecord;
|
|
import com.android.apksig.internal.zip.LocalFileRecord;
|
|
import com.android.apksig.internal.zip.ZipUtils;
|
|
import com.android.apksig.util.DataSink;
|
|
import com.android.apksig.util.DataSinks;
|
|
import com.android.apksig.util.DataSource;
|
|
import com.android.apksig.util.DataSources;
|
|
import com.android.apksig.util.ReadableDataSink;
|
|
import com.android.apksig.zip.ZipFormatException;
|
|
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.PrivateKey;
|
|
import java.security.SignatureException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* APK signer.
|
|
*
|
|
* <p>The signer preserves as much of the input APK as possible. For example, it preserves the order
|
|
* of APK entries and preserves their contents, including compressed form and alignment of data.
|
|
*
|
|
* <p>Use {@link Builder} to obtain instances of this signer.
|
|
*
|
|
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
|
*/
|
|
public class ApkSigner {
|
|
|
|
/**
|
|
* Extensible data block/field header ID used for storing information about alignment of
|
|
* uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
|
|
* 4.5 Extensible data fields.
|
|
*/
|
|
private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
|
|
|
|
/**
|
|
* Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
|
|
* entries.
|
|
*/
|
|
private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
|
|
|
|
private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
|
|
|
|
/** Name of the Android manifest ZIP entry in APKs. */
|
|
private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
|
|
|
|
private final List<SignerConfig> mSignerConfigs;
|
|
private final SignerConfig mSourceStampSignerConfig;
|
|
private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
|
|
private final boolean mForceSourceStampOverwrite;
|
|
private final Integer mMinSdkVersion;
|
|
private final boolean mV1SigningEnabled;
|
|
private final boolean mV2SigningEnabled;
|
|
private final boolean mV3SigningEnabled;
|
|
private final boolean mV4SigningEnabled;
|
|
private final boolean mVerityEnabled;
|
|
private final boolean mV4ErrorReportingEnabled;
|
|
private final boolean mDebuggableApkPermitted;
|
|
private final boolean mOtherSignersSignaturesPreserved;
|
|
private final String mCreatedBy;
|
|
|
|
private final ApkSignerEngine mSignerEngine;
|
|
|
|
private final File mInputApkFile;
|
|
private final DataSource mInputApkDataSource;
|
|
|
|
private final File mOutputApkFile;
|
|
private final DataSink mOutputApkDataSink;
|
|
private final DataSource mOutputApkDataSource;
|
|
|
|
private final File mOutputV4File;
|
|
|
|
private final SigningCertificateLineage mSigningCertificateLineage;
|
|
|
|
private ApkSigner(
|
|
List<SignerConfig> signerConfigs,
|
|
SignerConfig sourceStampSignerConfig,
|
|
SigningCertificateLineage sourceStampSigningCertificateLineage,
|
|
boolean forceSourceStampOverwrite,
|
|
Integer minSdkVersion,
|
|
boolean v1SigningEnabled,
|
|
boolean v2SigningEnabled,
|
|
boolean v3SigningEnabled,
|
|
boolean v4SigningEnabled,
|
|
boolean verityEnabled,
|
|
boolean v4ErrorReportingEnabled,
|
|
boolean debuggableApkPermitted,
|
|
boolean otherSignersSignaturesPreserved,
|
|
String createdBy,
|
|
ApkSignerEngine signerEngine,
|
|
File inputApkFile,
|
|
DataSource inputApkDataSource,
|
|
File outputApkFile,
|
|
DataSink outputApkDataSink,
|
|
DataSource outputApkDataSource,
|
|
File outputV4File,
|
|
SigningCertificateLineage signingCertificateLineage) {
|
|
|
|
mSignerConfigs = signerConfigs;
|
|
mSourceStampSignerConfig = sourceStampSignerConfig;
|
|
mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
|
|
mForceSourceStampOverwrite = forceSourceStampOverwrite;
|
|
mMinSdkVersion = minSdkVersion;
|
|
mV1SigningEnabled = v1SigningEnabled;
|
|
mV2SigningEnabled = v2SigningEnabled;
|
|
mV3SigningEnabled = v3SigningEnabled;
|
|
mV4SigningEnabled = v4SigningEnabled;
|
|
mVerityEnabled = verityEnabled;
|
|
mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
|
|
mDebuggableApkPermitted = debuggableApkPermitted;
|
|
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
|
|
mCreatedBy = createdBy;
|
|
|
|
mSignerEngine = signerEngine;
|
|
|
|
mInputApkFile = inputApkFile;
|
|
mInputApkDataSource = inputApkDataSource;
|
|
|
|
mOutputApkFile = outputApkFile;
|
|
mOutputApkDataSink = outputApkDataSink;
|
|
mOutputApkDataSource = outputApkDataSource;
|
|
|
|
mOutputV4File = outputV4File;
|
|
|
|
mSigningCertificateLineage = signingCertificateLineage;
|
|
}
|
|
|
|
/**
|
|
* Signs the input APK and outputs the resulting signed APK. The input APK is not modified.
|
|
*
|
|
* @throws IOException if an I/O error is encountered while reading or writing the APKs
|
|
* @throws ApkFormatException if the input APK is malformed
|
|
* @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
|
|
* a required cryptographic algorithm implementation is missing
|
|
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
|
* not suitable for generating the signature
|
|
* @throws SignatureException if an error occurred while generating or verifying a signature
|
|
* @throws IllegalStateException if this signer's configuration is missing required information
|
|
* or if the signing engine is in an invalid state.
|
|
*/
|
|
public void sign()
|
|
throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
|
SignatureException, IllegalStateException {
|
|
Closeable in = null;
|
|
DataSource inputApk;
|
|
try {
|
|
if (mInputApkDataSource != null) {
|
|
inputApk = mInputApkDataSource;
|
|
} else if (mInputApkFile != null) {
|
|
RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r");
|
|
in = inputFile;
|
|
inputApk = DataSources.asDataSource(inputFile);
|
|
} else {
|
|
throw new IllegalStateException("Input APK not specified");
|
|
}
|
|
|
|
Closeable out = null;
|
|
try {
|
|
DataSink outputApkOut;
|
|
DataSource outputApkIn;
|
|
if (mOutputApkDataSink != null) {
|
|
outputApkOut = mOutputApkDataSink;
|
|
outputApkIn = mOutputApkDataSource;
|
|
} else if (mOutputApkFile != null) {
|
|
RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw");
|
|
out = outputFile;
|
|
outputFile.setLength(0);
|
|
outputApkOut = DataSinks.asDataSink(outputFile);
|
|
outputApkIn = DataSources.asDataSource(outputFile);
|
|
} else {
|
|
throw new IllegalStateException("Output APK not specified");
|
|
}
|
|
|
|
sign(inputApk, outputApkOut, outputApkIn);
|
|
} finally {
|
|
if (out != null) {
|
|
out.close();
|
|
}
|
|
}
|
|
} finally {
|
|
if (in != null) {
|
|
in.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn)
|
|
throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
|
SignatureException {
|
|
// Step 1. Find input APK's main ZIP sections
|
|
ApkUtils.ZipSections inputZipSections;
|
|
try {
|
|
inputZipSections = ApkUtils.findZipSections(inputApk);
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
|
|
}
|
|
long inputApkSigningBlockOffset = -1;
|
|
DataSource inputApkSigningBlock = null;
|
|
try {
|
|
ApkUtils.ApkSigningBlock apkSigningBlockInfo =
|
|
ApkUtils.findApkSigningBlock(inputApk, inputZipSections);
|
|
inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
|
|
inputApkSigningBlock = apkSigningBlockInfo.getContents();
|
|
} catch (ApkSigningBlockNotFoundException e) {
|
|
// Input APK does not contain an APK Signing Block. That's OK. APKs are not required to
|
|
// contain this block. It's only needed if the APK is signed using APK Signature Scheme
|
|
// v2 and/or v3.
|
|
}
|
|
DataSource inputApkLfhSection =
|
|
inputApk.slice(
|
|
0,
|
|
(inputApkSigningBlockOffset != -1)
|
|
? inputApkSigningBlockOffset
|
|
: inputZipSections.getZipCentralDirectoryOffset());
|
|
|
|
// Step 2. Parse the input APK's ZIP Central Directory
|
|
ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
|
|
List<CentralDirectoryRecord> inputCdRecords =
|
|
parseZipCentralDirectory(inputCd, inputZipSections);
|
|
|
|
List<Hints.PatternWithRange> pinPatterns =
|
|
extractPinPatterns(inputCdRecords, inputApkLfhSection);
|
|
List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
|
|
|
|
// Step 3. Obtain a signer engine instance
|
|
ApkSignerEngine signerEngine;
|
|
if (mSignerEngine != null) {
|
|
// Use the provided signer engine
|
|
signerEngine = mSignerEngine;
|
|
} else {
|
|
// Construct a signer engine from the provided parameters
|
|
int minSdkVersion;
|
|
if (mMinSdkVersion != null) {
|
|
// No need to extract minSdkVersion from the APK's AndroidManifest.xml
|
|
minSdkVersion = mMinSdkVersion;
|
|
} else {
|
|
// Need to extract minSdkVersion from the APK's AndroidManifest.xml
|
|
minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection);
|
|
}
|
|
List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
|
|
new ArrayList<>(mSignerConfigs.size());
|
|
for (SignerConfig signerConfig : mSignerConfigs) {
|
|
engineSignerConfigs.add(
|
|
new DefaultApkSignerEngine.SignerConfig.Builder(
|
|
signerConfig.getName(),
|
|
signerConfig.getPrivateKey(),
|
|
signerConfig.getCertificates(),
|
|
signerConfig.getDeterministicDsaSigning())
|
|
.build());
|
|
}
|
|
DefaultApkSignerEngine.Builder signerEngineBuilder =
|
|
new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
|
|
.setV1SigningEnabled(mV1SigningEnabled)
|
|
.setV2SigningEnabled(mV2SigningEnabled)
|
|
.setV3SigningEnabled(mV3SigningEnabled)
|
|
.setVerityEnabled(mVerityEnabled)
|
|
.setDebuggableApkPermitted(mDebuggableApkPermitted)
|
|
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
|
|
.setSigningCertificateLineage(mSigningCertificateLineage);
|
|
if (mCreatedBy != null) {
|
|
signerEngineBuilder.setCreatedBy(mCreatedBy);
|
|
}
|
|
if (mSourceStampSignerConfig != null) {
|
|
signerEngineBuilder.setStampSignerConfig(
|
|
new DefaultApkSignerEngine.SignerConfig.Builder(
|
|
mSourceStampSignerConfig.getName(),
|
|
mSourceStampSignerConfig.getPrivateKey(),
|
|
mSourceStampSignerConfig.getCertificates(),
|
|
mSourceStampSignerConfig.getDeterministicDsaSigning())
|
|
.build());
|
|
}
|
|
if (mSourceStampSigningCertificateLineage != null) {
|
|
signerEngineBuilder.setSourceStampSigningCertificateLineage(
|
|
mSourceStampSigningCertificateLineage);
|
|
}
|
|
signerEngine = signerEngineBuilder.build();
|
|
}
|
|
|
|
// Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
|
|
if (inputApkSigningBlock != null) {
|
|
signerEngine.inputApkSigningBlock(inputApkSigningBlock);
|
|
}
|
|
|
|
// Step 5. Iterate over input APK's entries and output the Local File Header + data of those
|
|
// entries which need to be output. Entries are iterated in the order in which their Local
|
|
// File Header records are stored in the file. This is to achieve better data locality in
|
|
// case Central Directory entries are in the wrong order.
|
|
List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
|
|
new ArrayList<>(inputCdRecords);
|
|
Collections.sort(
|
|
inputCdRecordsSortedByLfhOffset,
|
|
CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
|
|
int lastModifiedDateForNewEntries = -1;
|
|
int lastModifiedTimeForNewEntries = -1;
|
|
long inputOffset = 0;
|
|
long outputOffset = 0;
|
|
byte[] sourceStampCertificateDigest = null;
|
|
Map<String, CentralDirectoryRecord> outputCdRecordsByName =
|
|
new HashMap<>(inputCdRecords.size());
|
|
for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
|
|
String entryName = inputCdRecord.getName();
|
|
if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
|
|
continue; // We'll re-add below if needed.
|
|
}
|
|
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
|
|
try {
|
|
sourceStampCertificateDigest =
|
|
LocalFileRecord.getUncompressedData(
|
|
inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
|
|
} catch (ZipFormatException ex) {
|
|
throw new ApkFormatException("Bad source stamp entry");
|
|
}
|
|
continue; // Existing source stamp is handled below as needed.
|
|
}
|
|
ApkSignerEngine.InputJarEntryInstructions entryInstructions =
|
|
signerEngine.inputJarEntry(entryName);
|
|
boolean shouldOutput;
|
|
switch (entryInstructions.getOutputPolicy()) {
|
|
case OUTPUT:
|
|
shouldOutput = true;
|
|
break;
|
|
case OUTPUT_BY_ENGINE:
|
|
case SKIP:
|
|
shouldOutput = false;
|
|
break;
|
|
default:
|
|
throw new RuntimeException(
|
|
"Unknown output policy: " + entryInstructions.getOutputPolicy());
|
|
}
|
|
|
|
long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset();
|
|
if (inputLocalFileHeaderStartOffset > inputOffset) {
|
|
// Unprocessed data in input starting at inputOffset and ending and the start of
|
|
// this record's LFH. We output this data verbatim because this signer is supposed
|
|
// to preserve as much of input as possible.
|
|
long chunkSize = inputLocalFileHeaderStartOffset - inputOffset;
|
|
inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
|
|
outputOffset += chunkSize;
|
|
inputOffset = inputLocalFileHeaderStartOffset;
|
|
}
|
|
LocalFileRecord inputLocalFileRecord;
|
|
try {
|
|
inputLocalFileRecord =
|
|
LocalFileRecord.getRecord(
|
|
inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e);
|
|
}
|
|
inputOffset += inputLocalFileRecord.getSize();
|
|
|
|
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
|
entryInstructions.getInspectJarEntryRequest();
|
|
if (inspectEntryRequest != null) {
|
|
fulfillInspectInputJarEntryRequest(
|
|
inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
|
|
}
|
|
|
|
if (shouldOutput) {
|
|
// Find the max value of last modified, to be used for new entries added by the
|
|
// signer.
|
|
int lastModifiedDate = inputCdRecord.getLastModificationDate();
|
|
int lastModifiedTime = inputCdRecord.getLastModificationTime();
|
|
if ((lastModifiedDateForNewEntries == -1)
|
|
|| (lastModifiedDate > lastModifiedDateForNewEntries)
|
|
|| ((lastModifiedDate == lastModifiedDateForNewEntries)
|
|
&& (lastModifiedTime > lastModifiedTimeForNewEntries))) {
|
|
lastModifiedDateForNewEntries = lastModifiedDate;
|
|
lastModifiedTimeForNewEntries = lastModifiedTime;
|
|
}
|
|
|
|
inspectEntryRequest = signerEngine.outputJarEntry(entryName);
|
|
if (inspectEntryRequest != null) {
|
|
fulfillInspectInputJarEntryRequest(
|
|
inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
|
|
}
|
|
|
|
// Output entry's Local File Header + data
|
|
long outputLocalFileHeaderOffset = outputOffset;
|
|
OutputSizeAndDataOffset outputLfrResult =
|
|
outputInputJarEntryLfhRecordPreservingDataAlignment(
|
|
inputApkLfhSection,
|
|
inputLocalFileRecord,
|
|
outputApkOut,
|
|
outputLocalFileHeaderOffset);
|
|
outputOffset += outputLfrResult.outputBytes;
|
|
long outputDataOffset =
|
|
outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes;
|
|
|
|
if (pinPatterns != null) {
|
|
boolean pinFileHeader = false;
|
|
for (Hints.PatternWithRange pinPattern : pinPatterns) {
|
|
if (pinPattern.matcher(inputCdRecord.getName()).matches()) {
|
|
Hints.ByteRange dataRange =
|
|
new Hints.ByteRange(outputDataOffset, outputOffset);
|
|
Hints.ByteRange pinRange =
|
|
pinPattern.ClampToAbsoluteByteRange(dataRange);
|
|
if (pinRange != null) {
|
|
pinFileHeader = true;
|
|
pinByteRanges.add(pinRange);
|
|
}
|
|
}
|
|
}
|
|
if (pinFileHeader) {
|
|
pinByteRanges.add(
|
|
new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset));
|
|
}
|
|
}
|
|
|
|
// Enqueue entry's Central Directory record for output
|
|
CentralDirectoryRecord outputCdRecord;
|
|
if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) {
|
|
outputCdRecord = inputCdRecord;
|
|
} else {
|
|
outputCdRecord =
|
|
inputCdRecord.createWithModifiedLocalFileHeaderOffset(
|
|
outputLocalFileHeaderOffset);
|
|
}
|
|
outputCdRecordsByName.put(entryName, outputCdRecord);
|
|
}
|
|
}
|
|
long inputLfhSectionSize = inputApkLfhSection.size();
|
|
if (inputOffset < inputLfhSectionSize) {
|
|
// Unprocessed data in input starting at inputOffset and ending and the end of the input
|
|
// APK's LFH section. We output this data verbatim because this signer is supposed
|
|
// to preserve as much of input as possible.
|
|
long chunkSize = inputLfhSectionSize - inputOffset;
|
|
inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
|
|
outputOffset += chunkSize;
|
|
inputOffset = inputLfhSectionSize;
|
|
}
|
|
|
|
// Step 6. Sort output APK's Central Directory records in the order in which they should
|
|
// appear in the output
|
|
List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
|
|
for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
|
|
String entryName = inputCdRecord.getName();
|
|
CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
|
|
if (outputCdRecord != null) {
|
|
outputCdRecords.add(outputCdRecord);
|
|
}
|
|
}
|
|
|
|
if (lastModifiedDateForNewEntries == -1) {
|
|
lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
|
|
lastModifiedTimeForNewEntries = 0;
|
|
}
|
|
|
|
// Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output
|
|
// more Local File Header + data entries and add to the list of output Central Directory
|
|
// records.
|
|
if (signerEngine.isEligibleForSourceStamp()) {
|
|
byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
|
|
if (mForceSourceStampOverwrite
|
|
|| sourceStampCertificateDigest == null
|
|
|| Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
|
|
outputOffset +=
|
|
outputDataToOutputApk(
|
|
SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
|
|
uncompressedData,
|
|
outputOffset,
|
|
outputCdRecords,
|
|
lastModifiedTimeForNewEntries,
|
|
lastModifiedDateForNewEntries,
|
|
outputApkOut);
|
|
} else {
|
|
throw new ApkFormatException(
|
|
String.format(
|
|
"Cannot generate SourceStamp. APK contains an existing entry with"
|
|
+ " the name: %s, and it is different than the provided source"
|
|
+ " stamp certificate",
|
|
SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
|
|
}
|
|
}
|
|
|
|
// Step 7.5. Generate pinlist.meta file if necessary.
|
|
// This has to be before the step 8 so that the file is signed.
|
|
if (pinByteRanges != null) {
|
|
// Covers JAR signature and zip central dir entry.
|
|
// The signature files don't have to be pinned, but pinning them isn't that wasteful
|
|
// since the total size is small.
|
|
pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));
|
|
String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
|
|
byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
|
|
|
|
requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
|
|
outputOffset +=
|
|
outputDataToOutputApk(
|
|
entryName,
|
|
uncompressedData,
|
|
outputOffset,
|
|
outputCdRecords,
|
|
lastModifiedTimeForNewEntries,
|
|
lastModifiedDateForNewEntries,
|
|
outputApkOut);
|
|
}
|
|
|
|
// Step 8. Generate and output JAR signatures, if necessary. This may output more Local File
|
|
// Header + data entries and add to the list of output Central Directory records.
|
|
ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
|
|
signerEngine.outputJarEntries();
|
|
if (outputJarSignatureRequest != null) {
|
|
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
|
|
outputJarSignatureRequest.getAdditionalJarEntries()) {
|
|
String entryName = entry.getName();
|
|
byte[] uncompressedData = entry.getData();
|
|
|
|
requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
|
|
outputOffset +=
|
|
outputDataToOutputApk(
|
|
entryName,
|
|
uncompressedData,
|
|
outputOffset,
|
|
outputCdRecords,
|
|
lastModifiedTimeForNewEntries,
|
|
lastModifiedDateForNewEntries,
|
|
outputApkOut);
|
|
}
|
|
outputJarSignatureRequest.done();
|
|
}
|
|
|
|
// Step 9. Construct output ZIP Central Directory in an in-memory buffer
|
|
long outputCentralDirSizeBytes = 0;
|
|
for (CentralDirectoryRecord record : outputCdRecords) {
|
|
outputCentralDirSizeBytes += record.getSize();
|
|
}
|
|
if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
|
|
throw new IOException(
|
|
"Output ZIP Central Directory too large: "
|
|
+ outputCentralDirSizeBytes
|
|
+ " bytes");
|
|
}
|
|
ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
|
|
for (CentralDirectoryRecord record : outputCdRecords) {
|
|
record.copyTo(outputCentralDir);
|
|
}
|
|
outputCentralDir.flip();
|
|
DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
|
|
long outputCentralDirStartOffset = outputOffset;
|
|
int outputCentralDirRecordCount = outputCdRecords.size();
|
|
|
|
// Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer
|
|
ByteBuffer outputEocd =
|
|
EocdRecord.createWithModifiedCentralDirectoryInfo(
|
|
inputZipSections.getZipEndOfCentralDirectory(),
|
|
outputCentralDirRecordCount,
|
|
outputCentralDirDataSource.size(),
|
|
outputCentralDirStartOffset);
|
|
|
|
// Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or
|
|
// SourceStamp signatures, if necessary.
|
|
// This may insert an APK Signing Block just before the output's ZIP Central Directory
|
|
ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
|
|
signerEngine.outputZipSections2(
|
|
outputApkIn,
|
|
outputCentralDirDataSource,
|
|
DataSources.asDataSource(outputEocd));
|
|
|
|
if (outputApkSigningBlockRequest != null) {
|
|
int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock();
|
|
outputApkOut.consume(ByteBuffer.allocate(padding));
|
|
byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
|
|
outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
|
|
ZipUtils.setZipEocdCentralDirectoryOffset(
|
|
outputEocd,
|
|
outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
|
|
outputApkSigningBlockRequest.done();
|
|
}
|
|
|
|
// Step 12. Output ZIP Central Directory and ZIP End of Central Directory
|
|
outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
|
|
outputApkOut.consume(outputEocd);
|
|
signerEngine.outputDone();
|
|
|
|
// Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
|
|
if (mV4SigningEnabled) {
|
|
signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled);
|
|
}
|
|
}
|
|
|
|
private static void requestOutputEntryInspection(
|
|
ApkSignerEngine signerEngine,
|
|
String entryName,
|
|
byte[] uncompressedData)
|
|
throws IOException {
|
|
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
|
signerEngine.outputJarEntry(entryName);
|
|
if (inspectEntryRequest != null) {
|
|
inspectEntryRequest.getDataSink().consume(
|
|
uncompressedData, 0, uncompressedData.length);
|
|
inspectEntryRequest.done();
|
|
}
|
|
}
|
|
|
|
private static long outputDataToOutputApk(
|
|
String entryName,
|
|
byte[] uncompressedData,
|
|
long localFileHeaderOffset,
|
|
List<CentralDirectoryRecord> outputCdRecords,
|
|
int lastModifiedTimeForNewEntries,
|
|
int lastModifiedDateForNewEntries,
|
|
DataSink outputApkOut)
|
|
throws IOException {
|
|
ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
|
|
byte[] compressedData = deflateResult.output;
|
|
long uncompressedDataCrc32 = deflateResult.inputCrc32;
|
|
long numOfDataBytes =
|
|
LocalFileRecord.outputRecordWithDeflateCompressedData(
|
|
entryName,
|
|
lastModifiedTimeForNewEntries,
|
|
lastModifiedDateForNewEntries,
|
|
compressedData,
|
|
uncompressedDataCrc32,
|
|
uncompressedData.length,
|
|
outputApkOut);
|
|
outputCdRecords.add(
|
|
CentralDirectoryRecord.createWithDeflateCompressedData(
|
|
entryName,
|
|
lastModifiedTimeForNewEntries,
|
|
lastModifiedDateForNewEntries,
|
|
uncompressedDataCrc32,
|
|
compressedData.length,
|
|
uncompressedData.length,
|
|
localFileHeaderOffset));
|
|
return numOfDataBytes;
|
|
}
|
|
|
|
private static void fulfillInspectInputJarEntryRequest(
|
|
DataSource lfhSection,
|
|
LocalFileRecord localFileRecord,
|
|
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
|
|
throws IOException, ApkFormatException {
|
|
try {
|
|
localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.getName(), e);
|
|
}
|
|
inspectEntryRequest.done();
|
|
}
|
|
|
|
private static class OutputSizeAndDataOffset {
|
|
public long outputBytes;
|
|
public long dataOffsetBytes;
|
|
|
|
public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) {
|
|
this.outputBytes = outputBytes;
|
|
this.dataOffsetBytes = dataOffsetBytes;
|
|
}
|
|
}
|
|
|
|
private static OutputSizeAndDataOffset outputInputJarEntryLfhRecordPreservingDataAlignment(
|
|
DataSource inputLfhSection,
|
|
LocalFileRecord inputRecord,
|
|
DataSink outputLfhSection,
|
|
long outputOffset)
|
|
throws IOException {
|
|
long inputOffset = inputRecord.getStartOffsetInArchive();
|
|
if (inputOffset == outputOffset) {
|
|
// This record's data will be aligned same as in the input APK.
|
|
return new OutputSizeAndDataOffset(
|
|
inputRecord.outputRecord(inputLfhSection, outputLfhSection),
|
|
inputRecord.getDataStartOffsetInRecord());
|
|
}
|
|
int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
|
|
if ((dataAlignmentMultiple <= 1)
|
|
|| ((inputOffset % dataAlignmentMultiple)
|
|
== (outputOffset % dataAlignmentMultiple))) {
|
|
// This record's data will be aligned same as in the input APK.
|
|
return new OutputSizeAndDataOffset(
|
|
inputRecord.outputRecord(inputLfhSection, outputLfhSection),
|
|
inputRecord.getDataStartOffsetInRecord());
|
|
}
|
|
|
|
long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
|
|
if ((inputDataStartOffset % dataAlignmentMultiple) != 0) {
|
|
// This record's data is not aligned in the input APK. No need to align it in the
|
|
// output.
|
|
return new OutputSizeAndDataOffset(
|
|
inputRecord.outputRecord(inputLfhSection, outputLfhSection),
|
|
inputRecord.getDataStartOffsetInRecord());
|
|
}
|
|
|
|
// This record's data needs to be re-aligned in the output. This is achieved using the
|
|
// record's extra field.
|
|
ByteBuffer aligningExtra =
|
|
createExtraFieldToAlignData(
|
|
inputRecord.getExtra(),
|
|
outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
|
|
dataAlignmentMultiple);
|
|
long dataOffset =
|
|
(long) inputRecord.getDataStartOffsetInRecord()
|
|
+ aligningExtra.remaining()
|
|
- inputRecord.getExtra().remaining();
|
|
return new OutputSizeAndDataOffset(
|
|
inputRecord.outputRecordWithModifiedExtra(
|
|
inputLfhSection, aligningExtra, outputLfhSection),
|
|
dataOffset);
|
|
}
|
|
|
|
private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
|
|
if (entry.isDataCompressed()) {
|
|
// Compressed entries don't need to be aligned
|
|
return 1;
|
|
}
|
|
|
|
// Attempt to obtain the alignment multiple from the entry's extra field.
|
|
ByteBuffer extra = entry.getExtra();
|
|
if (extra.hasRemaining()) {
|
|
extra.order(ByteOrder.LITTLE_ENDIAN);
|
|
// FORMAT: sequence of fields. Each field consists of:
|
|
// * uint16 ID
|
|
// * uint16 size
|
|
// * 'size' bytes: payload
|
|
while (extra.remaining() >= 4) {
|
|
short headerId = extra.getShort();
|
|
int dataSize = ZipUtils.getUnsignedInt16(extra);
|
|
if (dataSize > extra.remaining()) {
|
|
// Malformed field -- insufficient input remaining
|
|
break;
|
|
}
|
|
if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
|
|
// Skip this field
|
|
extra.position(extra.position() + dataSize);
|
|
continue;
|
|
}
|
|
// This is APK alignment field.
|
|
// FORMAT:
|
|
// * uint16 alignment multiple (in bytes)
|
|
// * remaining bytes -- padding to achieve alignment of data which starts after
|
|
// the extra field
|
|
if (dataSize < 2) {
|
|
// Malformed
|
|
break;
|
|
}
|
|
return ZipUtils.getUnsignedInt16(extra);
|
|
}
|
|
}
|
|
|
|
// Fall back to filename-based defaults
|
|
return (entry.getName().endsWith(".so")) ? ANDROID_COMMON_PAGE_ALIGNMENT_BYTES : 4;
|
|
}
|
|
|
|
private static ByteBuffer createExtraFieldToAlignData(
|
|
ByteBuffer original, long extraStartOffset, int dataAlignmentMultiple) {
|
|
if (dataAlignmentMultiple <= 1) {
|
|
return original;
|
|
}
|
|
|
|
// In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1.
|
|
ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple);
|
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
// Step 1. Output all extra fields other than the one which is to do with alignment
|
|
// FORMAT: sequence of fields. Each field consists of:
|
|
// * uint16 ID
|
|
// * uint16 size
|
|
// * 'size' bytes: payload
|
|
while (original.remaining() >= 4) {
|
|
short headerId = original.getShort();
|
|
int dataSize = ZipUtils.getUnsignedInt16(original);
|
|
if (dataSize > original.remaining()) {
|
|
// Malformed field -- insufficient input remaining
|
|
break;
|
|
}
|
|
if (((headerId == 0) && (dataSize == 0))
|
|
|| (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) {
|
|
// Ignore the field if it has to do with the old APK data alignment method (filling
|
|
// the extra field with 0x00 bytes) or the new APK data alignment method.
|
|
original.position(original.position() + dataSize);
|
|
continue;
|
|
}
|
|
// Copy this field (including header) to the output
|
|
original.position(original.position() - 4);
|
|
int originalLimit = original.limit();
|
|
original.limit(original.position() + 4 + dataSize);
|
|
result.put(original);
|
|
original.limit(originalLimit);
|
|
}
|
|
|
|
// Step 2. Add alignment field
|
|
// FORMAT:
|
|
// * uint16 extra header ID
|
|
// * uint16 extra data size
|
|
// Payload ('data size' bytes)
|
|
// * uint16 alignment multiple (in bytes)
|
|
// * remaining bytes -- padding to achieve alignment of data which starts after the
|
|
// extra field
|
|
long dataMinStartOffset =
|
|
extraStartOffset
|
|
+ result.position()
|
|
+ ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
|
|
int paddingSizeBytes =
|
|
(dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
|
|
% dataAlignmentMultiple;
|
|
result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
|
|
ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes);
|
|
ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple);
|
|
result.position(result.position() + paddingSizeBytes);
|
|
result.flip();
|
|
|
|
return result;
|
|
}
|
|
|
|
private static ByteBuffer getZipCentralDirectory(
|
|
DataSource apk, ApkUtils.ZipSections apkSections)
|
|
throws IOException, ApkFormatException {
|
|
long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
|
|
if (cdSizeBytes > Integer.MAX_VALUE) {
|
|
throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
|
|
}
|
|
long cdOffset = apkSections.getZipCentralDirectoryOffset();
|
|
ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
|
|
cd.order(ByteOrder.LITTLE_ENDIAN);
|
|
return cd;
|
|
}
|
|
|
|
private static List<CentralDirectoryRecord> parseZipCentralDirectory(
|
|
ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
|
|
long cdOffset = apkSections.getZipCentralDirectoryOffset();
|
|
int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
|
|
List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
|
|
Set<String> entryNames = new HashSet<>(expectedCdRecordCount);
|
|
for (int i = 0; i < expectedCdRecordCount; i++) {
|
|
CentralDirectoryRecord cdRecord;
|
|
int offsetInsideCd = cd.position();
|
|
try {
|
|
cdRecord = CentralDirectoryRecord.getRecord(cd);
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException(
|
|
"Malformed ZIP Central Directory record #"
|
|
+ (i + 1)
|
|
+ " at file offset "
|
|
+ (cdOffset + offsetInsideCd),
|
|
e);
|
|
}
|
|
String entryName = cdRecord.getName();
|
|
if (!entryNames.add(entryName)) {
|
|
throw new ApkFormatException(
|
|
"Multiple ZIP entries with the same name: " + entryName);
|
|
}
|
|
cdRecords.add(cdRecord);
|
|
}
|
|
if (cd.hasRemaining()) {
|
|
throw new ApkFormatException(
|
|
"Unused space at the end of ZIP Central Directory: "
|
|
+ cd.remaining()
|
|
+ " bytes starting at file offset "
|
|
+ (cdOffset + cd.position()));
|
|
}
|
|
|
|
return cdRecords;
|
|
}
|
|
|
|
private static CentralDirectoryRecord findCdRecord(
|
|
List<CentralDirectoryRecord> cdRecords, String name) {
|
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
|
if (name.equals(cdRecord.getName())) {
|
|
return cdRecord;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the contents of the APK's {@code AndroidManifest.xml} or {@code null} if this entry
|
|
* is not present in the APK.
|
|
*/
|
|
static ByteBuffer getAndroidManifestFromApk(
|
|
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
|
|
throws IOException, ApkFormatException, ZipFormatException {
|
|
CentralDirectoryRecord androidManifestCdRecord =
|
|
findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
|
|
if (androidManifestCdRecord == null) {
|
|
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
|
|
}
|
|
|
|
return ByteBuffer.wrap(
|
|
LocalFileRecord.getUncompressedData(
|
|
lhfSection, androidManifestCdRecord, lhfSection.size()));
|
|
}
|
|
|
|
/**
|
|
* Return list of pin patterns embedded in the pin pattern asset file. If no such file, return
|
|
* {@code null}.
|
|
*/
|
|
private static List<Hints.PatternWithRange> extractPinPatterns(
|
|
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
|
|
throws IOException, ApkFormatException {
|
|
CentralDirectoryRecord pinListCdRecord =
|
|
findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
|
|
List<Hints.PatternWithRange> pinPatterns = null;
|
|
if (pinListCdRecord != null) {
|
|
pinPatterns = new ArrayList<>();
|
|
byte[] patternBlob;
|
|
try {
|
|
patternBlob =
|
|
LocalFileRecord.getUncompressedData(
|
|
lhfSection, pinListCdRecord, lhfSection.size());
|
|
} catch (ZipFormatException ex) {
|
|
throw new ApkFormatException("Bad " + pinListCdRecord);
|
|
}
|
|
pinPatterns = Hints.parsePinPatterns(patternBlob);
|
|
}
|
|
return pinPatterns;
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum Android version (API Level) supported by the provided APK. This is based
|
|
* on the {@code android:minSdkVersion} attributes of the APK's {@code AndroidManifest.xml}.
|
|
*/
|
|
private static int getMinSdkVersionFromApk(
|
|
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
|
|
throws IOException, MinSdkVersionException {
|
|
ByteBuffer androidManifest;
|
|
try {
|
|
androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection);
|
|
} catch (ZipFormatException | ApkFormatException e) {
|
|
throw new MinSdkVersionException(
|
|
"Failed to determine APK's minimum supported Android platform version", e);
|
|
}
|
|
return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
|
|
}
|
|
|
|
/**
|
|
* Configuration of a signer.
|
|
*
|
|
* <p>Use {@link Builder} to obtain configuration instances.
|
|
*/
|
|
public static class SignerConfig {
|
|
private final String mName;
|
|
private final PrivateKey mPrivateKey;
|
|
private final List<X509Certificate> mCertificates;
|
|
private boolean mDeterministicDsaSigning;
|
|
|
|
private SignerConfig(
|
|
String name,
|
|
PrivateKey privateKey,
|
|
List<X509Certificate> certificates,
|
|
boolean deterministicDsaSigning) {
|
|
mName = name;
|
|
mPrivateKey = privateKey;
|
|
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
|
|
mDeterministicDsaSigning = deterministicDsaSigning;
|
|
}
|
|
/** Returns the name of this signer. */
|
|
public String getName() {
|
|
return mName;
|
|
}
|
|
|
|
/** Returns the signing key of this signer. */
|
|
public PrivateKey getPrivateKey() {
|
|
return mPrivateKey;
|
|
}
|
|
|
|
/**
|
|
* Returns the certificate(s) of this signer. The first certificate's public key corresponds
|
|
* to this signer's private key.
|
|
*/
|
|
public List<X509Certificate> getCertificates() {
|
|
return mCertificates;
|
|
}
|
|
|
|
|
|
/**
|
|
* If this signer is a DSA signer, whether or not the signing is done deterministically.
|
|
*/
|
|
public boolean getDeterministicDsaSigning() {
|
|
return mDeterministicDsaSigning;
|
|
}
|
|
|
|
/** Builder of {@link SignerConfig} instances. */
|
|
public static class Builder {
|
|
private final String mName;
|
|
private final PrivateKey mPrivateKey;
|
|
private final List<X509Certificate> mCertificates;
|
|
private final boolean mDeterministicDsaSigning;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder}.
|
|
*
|
|
* @param name signer's name. The name is reflected in the name of files comprising the
|
|
* JAR signature of the APK.
|
|
* @param privateKey signing key
|
|
* @param certificates list of one or more X.509 certificates. The subject public key of
|
|
* the first certificate must correspond to the {@code privateKey}.
|
|
*/
|
|
public Builder(
|
|
String name,
|
|
PrivateKey privateKey,
|
|
List<X509Certificate> certificates) {
|
|
this(name, privateKey, certificates, false);
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder}.
|
|
*
|
|
* @param name signer's name. The name is reflected in the name of files comprising the
|
|
* JAR signature of the APK.
|
|
* @param privateKey signing key
|
|
* @param certificates list of one or more X.509 certificates. The subject public key of
|
|
* the first certificate must correspond to the {@code privateKey}.
|
|
* @param deterministicDsaSigning When signing using DSA, whether or not the
|
|
* deterministic variant (RFC6979) should be used.
|
|
*/
|
|
public Builder(
|
|
String name,
|
|
PrivateKey privateKey,
|
|
List<X509Certificate> certificates,
|
|
boolean deterministicDsaSigning) {
|
|
if (name.isEmpty()) {
|
|
throw new IllegalArgumentException("Empty name");
|
|
}
|
|
mName = name;
|
|
mPrivateKey = privateKey;
|
|
mCertificates = new ArrayList<>(certificates);
|
|
mDeterministicDsaSigning = deterministicDsaSigning;
|
|
}
|
|
|
|
/**
|
|
* Returns a new {@code SignerConfig} instance configured based on the configuration of
|
|
* this builder.
|
|
*/
|
|
public SignerConfig build() {
|
|
return new SignerConfig(mName, mPrivateKey, mCertificates,
|
|
mDeterministicDsaSigning);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builder of {@link ApkSigner} instances.
|
|
*
|
|
* <p>The builder requires the following information to construct a working {@code ApkSigner}:
|
|
*
|
|
* <ul>
|
|
* <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
|
|
* <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
|
|
* <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
|
|
* variants.
|
|
* </ul>
|
|
*/
|
|
public static class Builder {
|
|
private final List<SignerConfig> mSignerConfigs;
|
|
private SignerConfig mSourceStampSignerConfig;
|
|
private SigningCertificateLineage mSourceStampSigningCertificateLineage;
|
|
private boolean mForceSourceStampOverwrite = false;
|
|
private boolean mV1SigningEnabled = true;
|
|
private boolean mV2SigningEnabled = true;
|
|
private boolean mV3SigningEnabled = true;
|
|
private boolean mV4SigningEnabled = true;
|
|
private boolean mVerityEnabled = false;
|
|
private boolean mV4ErrorReportingEnabled = false;
|
|
private boolean mDebuggableApkPermitted = true;
|
|
private boolean mOtherSignersSignaturesPreserved;
|
|
private String mCreatedBy;
|
|
private Integer mMinSdkVersion;
|
|
|
|
private final ApkSignerEngine mSignerEngine;
|
|
|
|
private File mInputApkFile;
|
|
private DataSource mInputApkDataSource;
|
|
|
|
private File mOutputApkFile;
|
|
private DataSink mOutputApkDataSink;
|
|
private DataSource mOutputApkDataSource;
|
|
|
|
private File mOutputV4File;
|
|
|
|
private SigningCertificateLineage mSigningCertificateLineage;
|
|
|
|
// APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
|
|
// signing by default, but not require prior clients to update to explicitly disable v3
|
|
// signing for multiple signers, we modify the mV3SigningEnabled depending on the provided
|
|
// inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two
|
|
// extra variables to record whether or not mV3SigningEnabled has been set directly by a
|
|
// client and so should override the default behavior.
|
|
private boolean mV3SigningExplicitlyDisabled = false;
|
|
private boolean mV3SigningExplicitlyEnabled = false;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
|
|
* signer configurations. The resulting signer may be further customized through this
|
|
* builder's setters, such as {@link #setMinSdkVersion(int)}, {@link
|
|
* #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, {@link
|
|
* #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
|
|
*
|
|
* <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
|
|
* control over low-level details of signing is desired.
|
|
*/
|
|
public Builder(List<SignerConfig> signerConfigs) {
|
|
if (signerConfigs.isEmpty()) {
|
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
|
}
|
|
if (signerConfigs.size() > 1) {
|
|
// APK Signature Scheme v3 only supports single signer, unless a
|
|
// SigningCertificateLineage is provided, in which case this will be reset to true,
|
|
// since we don't yet have a v4 scheme about which to worry
|
|
mV3SigningEnabled = false;
|
|
}
|
|
mSignerConfigs = new ArrayList<>(signerConfigs);
|
|
mSignerEngine = null;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
|
|
* signing engine. This is meant for advanced use cases where more control is needed over
|
|
* the lower-level details of signing. For typical use cases, {@link #Builder(List)} is more
|
|
* appropriate.
|
|
*/
|
|
public Builder(ApkSignerEngine signerEngine) {
|
|
if (signerEngine == null) {
|
|
throw new NullPointerException("signerEngine == null");
|
|
}
|
|
mSignerEngine = signerEngine;
|
|
mSignerConfigs = null;
|
|
}
|
|
|
|
/** Sets the signing configuration of the source stamp to be embedded in the APK. */
|
|
public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) {
|
|
mSourceStampSignerConfig = sourceStampSignerConfig;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
|
|
* signing certificate rotation for certificates previously used to sign source stamps.
|
|
*/
|
|
public Builder setSourceStampSigningCertificateLineage(
|
|
SigningCertificateLineage sourceStampSigningCertificateLineage) {
|
|
mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should overwrite existing source stamp, if found.
|
|
*
|
|
* @param force {@code true} to require the APK to be overwrite existing source stamp
|
|
*/
|
|
public Builder setForceSourceStampOverwrite(boolean force) {
|
|
mForceSourceStampOverwrite = force;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the APK to be signed.
|
|
*
|
|
* @see #setInputApk(DataSource)
|
|
*/
|
|
public Builder setInputApk(File inputApk) {
|
|
if (inputApk == null) {
|
|
throw new NullPointerException("inputApk == null");
|
|
}
|
|
mInputApkFile = inputApk;
|
|
mInputApkDataSource = null;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the APK to be signed.
|
|
*
|
|
* @see #setInputApk(File)
|
|
*/
|
|
public Builder setInputApk(DataSource inputApk) {
|
|
if (inputApk == null) {
|
|
throw new NullPointerException("inputApk == null");
|
|
}
|
|
mInputApkDataSource = inputApk;
|
|
mInputApkFile = null;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if
|
|
* it doesn't exist.
|
|
*
|
|
* @see #setOutputApk(ReadableDataSink)
|
|
* @see #setOutputApk(DataSink, DataSource)
|
|
*/
|
|
public Builder setOutputApk(File outputApk) {
|
|
if (outputApk == null) {
|
|
throw new NullPointerException("outputApk == null");
|
|
}
|
|
mOutputApkFile = outputApk;
|
|
mOutputApkDataSink = null;
|
|
mOutputApkDataSource = null;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the readable data sink which will receive the output (signed) APK. After signing,
|
|
* the contents of the output APK will be available via the {@link DataSource} interface of
|
|
* the sink.
|
|
*
|
|
* <p>This variant of {@code setOutputApk} is useful for avoiding writing the output APK to
|
|
* a file. For example, an in-memory data sink, such as {@link
|
|
* DataSinks#newInMemoryDataSink()}, could be used instead of a file.
|
|
*
|
|
* @see #setOutputApk(File)
|
|
* @see #setOutputApk(DataSink, DataSource)
|
|
*/
|
|
public Builder setOutputApk(ReadableDataSink outputApk) {
|
|
if (outputApk == null) {
|
|
throw new NullPointerException("outputApk == null");
|
|
}
|
|
return setOutputApk(outputApk, outputApk);
|
|
}
|
|
|
|
/**
|
|
* Sets the sink which will receive the output (signed) APK. Data received by the {@code
|
|
* outputApkOut} sink must be visible through the {@code outputApkIn} data source.
|
|
*
|
|
* <p>This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the
|
|
* sink and the source to be different objects.
|
|
*
|
|
* @see #setOutputApk(ReadableDataSink)
|
|
* @see #setOutputApk(File)
|
|
*/
|
|
public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) {
|
|
if (outputApkOut == null) {
|
|
throw new NullPointerException("outputApkOut == null");
|
|
}
|
|
if (outputApkIn == null) {
|
|
throw new NullPointerException("outputApkIn == null");
|
|
}
|
|
mOutputApkFile = null;
|
|
mOutputApkDataSink = outputApkOut;
|
|
mOutputApkDataSource = outputApkIn;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the location of the V4 output file. {@code ApkSigner} will create this file if it
|
|
* doesn't exist.
|
|
*/
|
|
public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) {
|
|
if (v4SignatureOutputFile == null) {
|
|
throw new NullPointerException("v4HashRootOutputFile == null");
|
|
}
|
|
mOutputV4File = v4SignatureOutputFile;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the minimum Android platform version (API Level) on which APK signatures produced by
|
|
* the signer being built must verify. This method is useful for overriding the default
|
|
* behavior where the minimum API Level is obtained from the {@code android:minSdkVersion}
|
|
* attribute of the APK's {@code AndroidManifest.xml}.
|
|
*
|
|
* <p><em>Note:</em> This method may result in APK signatures which don't verify on some
|
|
* Android platform versions supported by the APK.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
*/
|
|
public Builder setMinSdkVersion(int minSdkVersion) {
|
|
checkInitializedWithoutEngine();
|
|
mMinSdkVersion = minSdkVersion;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
|
|
*
|
|
* <p>By default, whether APK is signed using JAR signing is determined by {@code
|
|
* ApkSigner}, based on the platform versions supported by the APK or specified using {@link
|
|
* #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which don't
|
|
* verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* @param enabled {@code true} to require the APK to be signed using JAR signing, {@code
|
|
* false} to require the APK to not be signed using JAR signing.
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
* @see <a
|
|
* href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR
|
|
* signing</a>
|
|
*/
|
|
public Builder setV1SigningEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mV1SigningEnabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
|
|
* scheme).
|
|
*
|
|
* <p>By default, whether APK is signed using APK Signature Scheme v2 is determined by
|
|
* {@code ApkSigner} based on the platform versions supported by the APK or specified using
|
|
* {@link #setMinSdkVersion(int)}.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
|
|
* v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2.
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature
|
|
* Scheme v2</a>
|
|
*/
|
|
public Builder setV2SigningEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mV2SigningEnabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
|
|
* scheme).
|
|
*
|
|
* <p>By default, whether APK is signed using APK Signature Scheme v3 is determined by
|
|
* {@code ApkSigner} based on the platform versions supported by the APK or specified using
|
|
* {@link #setMinSdkVersion(int)}.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* <p><em>Note:</em> APK Signature Scheme v3 only supports a single signing certificate, but
|
|
* may take multiple signers mapping to different targeted platform versions.
|
|
*
|
|
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
|
|
* v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3.
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
*/
|
|
public Builder setV3SigningEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mV3SigningEnabled = enabled;
|
|
if (enabled) {
|
|
mV3SigningExplicitlyEnabled = true;
|
|
} else {
|
|
mV3SigningExplicitlyDisabled = true;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should be signed using APK Signature Scheme v4.
|
|
*
|
|
* <p>V4 signing requires that the APK be v2 or v3 signed.
|
|
*
|
|
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
|
|
* or v3 and generate an v4 signature file
|
|
*/
|
|
public Builder setV4SigningEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mV4SigningEnabled = enabled;
|
|
mV4ErrorReportingEnabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether errors during v4 signing should be reported and halt the signing process.
|
|
*
|
|
* <p>Error reporting for v4 signing is disabled by default, but will be enabled if the
|
|
* caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful
|
|
* for tools that enable v4 signing by default but don't want to fail the signing process if
|
|
* the user did not explicitly request the v4 signing.
|
|
*
|
|
* @param enabled {@code false} to prevent errors encountered during the V4 signing from
|
|
* halting the signing process
|
|
*/
|
|
public Builder setV4ErrorReportingEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mV4ErrorReportingEnabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether to enable the verity signature algorithm for the v2 and v3 signature
|
|
* schemes.
|
|
*
|
|
* @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
|
|
* v2 and v3 signature blocks.
|
|
*/
|
|
public Builder setVerityEnabled(boolean enabled) {
|
|
checkInitializedWithoutEngine();
|
|
mVerityEnabled = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the APK should be signed even if it is marked as debuggable ({@code
|
|
* android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
|
|
* compatibility reasons, the default value of this setting is {@code true}.
|
|
*
|
|
* <p>It is dangerous to sign debuggable APKs with production/release keys because Android
|
|
* platform loosens security checks for such APKs. For example, arbitrary unauthorized code
|
|
* may be executed in the context of such an app by anybody with ADB shell access.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*/
|
|
public Builder setDebuggableApkPermitted(boolean permitted) {
|
|
checkInitializedWithoutEngine();
|
|
mDebuggableApkPermitted = permitted;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether signatures produced by signers other than the ones configured in this engine
|
|
* should be copied from the input APK to the output APK.
|
|
*
|
|
* <p>By default, signatures of other signers are omitted from the output APK.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
*/
|
|
public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
|
|
checkInitializedWithoutEngine();
|
|
mOtherSignersSignaturesPreserved = preserved;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the value of the {@code Created-By} field in JAR signature files.
|
|
*
|
|
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
|
|
* with an {@link ApkSignerEngine}.
|
|
*
|
|
* @throws IllegalStateException if this builder was initialized with an {@link
|
|
* ApkSignerEngine}
|
|
*/
|
|
public Builder setCreatedBy(String createdBy) {
|
|
checkInitializedWithoutEngine();
|
|
if (createdBy == null) {
|
|
throw new NullPointerException();
|
|
}
|
|
mCreatedBy = createdBy;
|
|
return this;
|
|
}
|
|
|
|
private void checkInitializedWithoutEngine() {
|
|
if (mSignerEngine != null) {
|
|
throw new IllegalStateException(
|
|
"Operation is not available when builder initialized with an engine");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
|
|
* structure provides proof of signing certificate rotation linking {@link SignerConfig}
|
|
* objects to previous ones.
|
|
*/
|
|
public Builder setSigningCertificateLineage(
|
|
SigningCertificateLineage signingCertificateLineage) {
|
|
if (signingCertificateLineage != null) {
|
|
mV3SigningEnabled = true;
|
|
mSigningCertificateLineage = signingCertificateLineage;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns a new {@code ApkSigner} instance initialized according to the configuration of
|
|
* this builder.
|
|
*/
|
|
public ApkSigner build() {
|
|
if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
|
|
throw new IllegalStateException(
|
|
"Builder configured to both enable and disable APK "
|
|
+ "Signature Scheme v3 signing");
|
|
}
|
|
|
|
if (mV3SigningExplicitlyDisabled) {
|
|
mV3SigningEnabled = false;
|
|
}
|
|
|
|
if (mV3SigningExplicitlyEnabled) {
|
|
mV3SigningEnabled = true;
|
|
}
|
|
|
|
// If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing
|
|
// must be disabled as well as it is dependent on V2/V3.
|
|
if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) {
|
|
if (!mV4ErrorReportingEnabled) {
|
|
mV4SigningEnabled = false;
|
|
} else {
|
|
throw new IllegalStateException(
|
|
"APK Signature Scheme v4 signing requires at least "
|
|
+ "v2 or v3 signing to be enabled");
|
|
}
|
|
}
|
|
|
|
// TODO - if v3 signing is enabled, check provided signers and history to see if valid
|
|
|
|
return new ApkSigner(
|
|
mSignerConfigs,
|
|
mSourceStampSignerConfig,
|
|
mSourceStampSigningCertificateLineage,
|
|
mForceSourceStampOverwrite,
|
|
mMinSdkVersion,
|
|
mV1SigningEnabled,
|
|
mV2SigningEnabled,
|
|
mV3SigningEnabled,
|
|
mV4SigningEnabled,
|
|
mVerityEnabled,
|
|
mV4ErrorReportingEnabled,
|
|
mDebuggableApkPermitted,
|
|
mOtherSignersSignaturesPreserved,
|
|
mCreatedBy,
|
|
mSignerEngine,
|
|
mInputApkFile,
|
|
mInputApkDataSource,
|
|
mOutputApkFile,
|
|
mOutputApkDataSink,
|
|
mOutputApkDataSource,
|
|
mOutputV4File,
|
|
mSigningCertificateLineage);
|
|
}
|
|
}
|
|
}
|