blob: 8cca444a715f7024778cbb8dd61a30bc890d54e4 [file] [log] [blame]
/*
* 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())
.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())
.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 SignerConfig(
String name, PrivateKey privateKey, List<X509Certificate> certificates) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
}
/** 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;
}
/** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
/**
* 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) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
}
/**
* Returns a new {@code SignerConfig} instance configured based on the configuration of
* this builder.
*/
public SignerConfig build() {
return new SignerConfig(mName, mPrivateKey, mCertificates);
}
}
}
/**
* 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);
}
}
}