diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8e01e9e7..5164e0a4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -10,8 +10,8 @@ android {
applicationId = "com.abdurazaaqmohammed.AntiSplit"
minSdk = 4
targetSdk = 35
- versionCode = 20
- versionName = "1.6.3.5"
+ versionCode = 21
+ versionName = "1.6.3.6"
multiDexEnabled = true
}
@@ -36,5 +36,6 @@ android {
dependencies {
implementation("com.android.support:multidex:1.0.3")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
+
}
}
\ No newline at end of file
diff --git a/app/src/main/assets/debug.keystore b/app/src/main/assets/debug.keystore
new file mode 100644
index 00000000..11c372d7
Binary files /dev/null and b/app/src/main/assets/debug.keystore differ
diff --git a/app/src/main/assets/testkey.past b/app/src/main/assets/testkey.past
deleted file mode 100644
index 37b7151e..00000000
Binary files a/app/src/main/assets/testkey.past and /dev/null differ
diff --git a/app/src/main/assets/testkey.pk8 b/app/src/main/assets/testkey.pk8
deleted file mode 100644
index 586c1bd5..00000000
Binary files a/app/src/main/assets/testkey.pk8 and /dev/null differ
diff --git a/app/src/main/java/com/abdurazaaqmohammed/AntiSplit/main/MainActivity.java b/app/src/main/java/com/abdurazaaqmohammed/AntiSplit/main/MainActivity.java
index c93ec2ba..79356e56 100644
--- a/app/src/main/java/com/abdurazaaqmohammed/AntiSplit/main/MainActivity.java
+++ b/app/src/main/java/com/abdurazaaqmohammed/AntiSplit/main/MainActivity.java
@@ -499,11 +499,11 @@ protected Void doInBackground(Uri... uris) {
xapkUri = null;
}
- try (OutputStream os = FileUtils.getOutputStream(uris[0], activity)) {
+ try{
Merger.run(
activity.urisAreSplitApks ? FileUtils.getInputStream(activity.splitAPKUri, activity) : null,
cacheDir,
- os,
+ uris[0],
xapkUri,
activity,
splits,
@@ -762,11 +762,12 @@ private void selectDirToSaveAPKOrSaveNow() {
getAntisplitMFolder() + File.separator + getOriginalFileName(this, splitAPKUri) // If originalFilePath is null urisAreSplitApks must be true because getNameFromNonSplitApks will always return something
: originalFilePath.replaceFirst("\\.(?:xapk|aspk|apk[sm])", "_antisplit.apk");
if(TextUtils.isEmpty(newFilePath) ||
- newFilePath.startsWith("/data/") || // when a file is shared it in /data/
- !(f = new File(newFilePath)).createNewFile() || f.canWrite()) {
+ newFilePath.startsWith("/data/") // when a file is shared it in /data/
+ // || !(f = new File(newFilePath)).createNewFile() || f.canWrite()
+ ) {
f = new File(getAntisplitMFolder(), newFilePath.substring(newFilePath.lastIndexOf(File.separator) + 1));
showError(rss.getString(R.string.no_filepath) + newFilePath);
- }
+ } else f = new File(newFilePath);
((TextView) findViewById(R.id.logField)).setText("");
LogUtil.logMessage(rss.getString(R.string.output) + f);
diff --git a/app/src/main/java/com/android/apksig/ApkSigner.java b/app/src/main/java/com/android/apksig/ApkSigner.java
new file mode 100644
index 00000000..37226175
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkSigner.java
@@ -0,0 +1,1678 @@
+/*
+ * 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 static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+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.apk.v3.V3SchemeConstants;
+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.
+ *
+ *
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.
+ *
+ *
Use {@link Builder} to obtain instances of this signer.
+ *
+ * @see Application Signing
+ */
+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;
+
+ private static final short ANDROID_FILE_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 mSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final boolean mForceSourceStampOverwrite;
+ private final Integer mMinSdkVersion;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mV3SigningEnabled;
+ private final boolean mV4SigningEnabled;
+ private final boolean mAlignFileSize;
+ 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 signerConfigs,
+ SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ boolean forceSourceStampOverwrite,
+ Integer minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean v4SigningEnabled,
+ boolean alignFileSize,
+ 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;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mV4SigningEnabled = v4SigningEnabled;
+ mAlignFileSize = alignFileSize;
+ 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 inputCdRecords =
+ parseZipCentralDirectory(inputCd, inputZipSections);
+
+ List pinPatterns =
+ extractPinPatterns(inputCdRecords, inputApkLfhSection);
+ List 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 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)
+ .setMinSdkVersionForRotation(mRotationMinSdkVersion)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease);
+ 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 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 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 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 RuntimeException(
+ "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
+ // because it can be adjusted in Step 11 due to signing block.
+ // - CD offset (it's shifted by signing block)
+ // - Comments (when the output file needs to be sized 4k-aligned)
+ 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();
+ byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+
+ long fileSize =
+ outputCentralDirStartOffset
+ + outputCentralDirDataSource.size()
+ + padding
+ + outputApkSigningBlock.length
+ + outputEocd.remaining();
+ if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) {
+ int eocdPadding =
+ (int)
+ (ANDROID_FILE_ALIGNMENT_BYTES
+ - fileSize % ANDROID_FILE_ALIGNMENT_BYTES);
+ // Replace EOCD with padding one so that output file size can be the multiples of
+ // alignment.
+ outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding);
+
+ // Since EoCD has changed, we need to regenerate signing block as well.
+ outputApkSigningBlockRequest =
+ signerEngine.outputZipSections2(
+ outputApkIn,
+ new ByteBufferDataSource(outputCentralDir),
+ DataSources.asDataSource(outputEocd));
+ outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+ }
+
+ outputApkOut.consume(ByteBuffer.allocate(padding));
+ outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+ ZipUtils.setZipEocdCentralDirectoryOffset(
+ outputEocd,
+ outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
+ }
+
+ // 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 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 parseZipCentralDirectory(
+ ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+ List cdRecords = new ArrayList<>(expectedCdRecordCount);
+ Set 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 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 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 extractPinPatterns(
+ List cdRecords, DataSource lhfSection)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord pinListCdRecord =
+ findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
+ List 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 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.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+ private boolean mDeterministicDsaSigning;
+
+ private SignerConfig(
+ String name,
+ PrivateKey privateKey,
+ List 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 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 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 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 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.
+ *
+ * The builder requires the following information to construct a working {@code ApkSigner}:
+ *
+ *
+ * - Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+ *
- APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+ *
- where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+ * variants.
+ *
+ */
+ public static class Builder {
+ private final List 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 mAlignFileSize = false;
+ private boolean mVerityEnabled = false;
+ private boolean mV4ErrorReportingEnabled = false;
+ private boolean mDebuggableApkPermitted = true;
+ private boolean mOtherSignersSignaturesPreserved;
+ private String mCreatedBy;
+ private Integer mMinSdkVersion;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
+
+ 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)}.
+ *
+ * {@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
+ * control over low-level details of signing is desired.
+ */
+ public Builder(List 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.
+ *
+ * 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.
+ *
+ *
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}.
+ *
+ *
Note: This method may result in APK signatures which don't verify on some
+ * Android platform versions supported by the APK.
+ *
+ *
Note: 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 the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop. This method is useful for overriding the
+ * default behavior where Android T is set as the minimum API level for rotation.
+ *
+ *
Note:Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ *
+ *
Note: 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 setMinSdkVersionForRotation(int minSdkVersion) {
+ checkInitializedWithoutEngine();
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ *
This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ *
Note:If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ *
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.
+ *
+ *
Note: 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 JAR
+ * signing
+ */
+ 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).
+ *
+ *
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)}.
+ *
+ *
Note: 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 APK Signature
+ * Scheme v2
+ */
+ 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).
+ *
+ *
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)}.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ *
Note: 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.
+ *
+ *
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.
+ *
+ *
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 the output APK files should be sized as multiples of 4K.
+ *
+ *
Note: 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 setAlignFileSize(boolean alignFileSize) {
+ checkInitializedWithoutEngine();
+ mAlignFileSize = alignFileSize;
+ 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}.
+ *
+ *
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.
+ *
+ *
Note: 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.
+ *
+ *
By default, signatures of other signers are omitted from the output APK.
+ *
+ *
Note: 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.
+ *
+ *
Note: 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,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mV4SigningEnabled,
+ mAlignFileSize,
+ mVerityEnabled,
+ mV4ErrorReportingEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mCreatedBy,
+ mSignerEngine,
+ mInputApkFile,
+ mInputApkDataSource,
+ mOutputApkFile,
+ mOutputApkDataSink,
+ mOutputApkDataSource,
+ mOutputV4File,
+ mSigningCertificateLineage);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/ApkSignerEngine.java b/app/src/main/java/com/android/apksig/ApkSignerEngine.java
new file mode 100644
index 00000000..c79f2327
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -0,0 +1,550 @@
+/*
+ * 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 com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK signing logic which is independent of how input and output APKs are stored, parsed, and
+ * generated.
+ *
+ *
Operating Model
+ *
+ * The abstract operating model is that there is an input APK which is being signed, thus producing
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
+ *
+ * The engine is stateful and thus cannot be used for signing multiple APKs. However, once
+ * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
+ * This may be more efficient than signing the APK using a new instance of the engine. See
+ * Incremental Operation.
+ *
+ *
In the engine's operating model, a signed APK is produced as follows.
+ *
+ * - JAR entries to be signed are output,
+ * - JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
+ * output,
+ * - JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
+ * to the output.
+ *
+ *
+ * The input APK may contain JAR entries which, depending on the engine's configuration, may or
+ * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
+ * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
+ * which tells the client whether the input JAR entry needs to be output. This avoids the need for
+ * the client to hard-code the aspects of APK signing which determine which parts of input must be
+ * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
+ * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
+ * APK.
+ *
+ *
To use the engine to sign an input APK (or a collection of JAR entries), follow these
+ * steps:
+ *
+ * - Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
+ * for signing multiple APKs.
+ * - Locate the input APK's APK Signing Block and provide it to
+ * {@link #inputApkSigningBlock(DataSource)}.
+ * - For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
+ * whether this entry should be output. The engine may request to inspect the entry.
+ * - For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
+ * inspect the entry.
+ * - Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
+ * that additional JAR entries are output. These entries comprise the output APK's JAR
+ * signature.
+ * - Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
+ * invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
+ * an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
+ * output APK's APK Signature Scheme v2 signature.
+ * - Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
+ * confirm that the output APK is signed.
+ * - Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
+ * engine free any resources it no longer needs.
+ *
+ *
+ * Some invocations of the engine may provide the client with a task to perform. The client is
+ * expected to perform all requested tasks before proceeding to the next stage of signing. See
+ * documentation of each method about the deadlines for performing the tasks requested by the
+ * method.
+ *
+ *
Incremental Operation
+ *
+ * The engine supports incremental operation where a signed APK is produced, then modified and
+ * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
+ * by the developer. Re-signing may be more efficient than signing from scratch.
+ *
+ * To use the engine in incremental mode, keep notifying the engine of changes to the APK through
+ * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
+ * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
+ * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
+ * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
+ * APK.
+ *
+ *
Output-only Operation
+ *
+ * The engine's abstract operating model consists of an input APK and an output APK. However, it is
+ * possible to use the engine in output-only mode where the engine's {@code input...} methods are
+ * not invoked. In this mode, the engine has less control over output because it cannot request that
+ * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
+ * signed and will report an error if cannot do so.
+ *
+ * @see Application Signing
+ */
+public interface ApkSignerEngine extends Closeable {
+
+ default void setExecutor(RunnablesExecutor executor) {
+ throw new UnsupportedOperationException("setExecutor method is not implemented");
+ }
+
+ /**
+ * Initializes the signer engine with the data already present in the apk (if any). There
+ * might already be data that can be reused if the entries has not been changed.
+ *
+ * @param manifestBytes
+ * @param entryNames
+ * @return set of entry names which were processed by the engine during the initialization, a
+ * subset of entryNames
+ */
+ default Set initWith(byte[] manifestBytes, Set entryNames) {
+ throw new UnsupportedOperationException("initWith method is not implemented");
+ }
+
+ /**
+ * Indicates to this engine that the input APK contains the provided APK Signing Block. The
+ * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
+ *
+ * @param apkSigningBlock APK signing block of the input APK. The provided data source is
+ * guaranteed to not be used by the engine after this method terminates.
+ *
+ * @throws IOException if an I/O error occurs while reading the APK Signing Block
+ * @throws ApkFormatException if the APK Signing Block is malformed
+ * @throws IllegalStateException if this engine is closed
+ */
+ void inputApkSigningBlock(DataSource apkSigningBlock)
+ throws IOException, ApkFormatException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was encountered in the input APK.
+ *
+ * When an input entry is updated/changed, it's OK to not invoke
+ * {@link #inputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return instructions about how to proceed with this entry
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was output.
+ *
+ *
It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
+ * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
+ * data requested by the engine.
+ *
+ *
When an already output entry is updated/changed, it's OK to not invoke
+ * {@link #outputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return request to inspect the entry or {@code null} if the engine does not need to inspect
+ * the entry. The request must be fulfilled before {@link #outputJarEntries()} is
+ * invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the input. It's safe
+ * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
+ *
+ * @return output policy of this JAR entry. The policy indicates how this input entry affects
+ * the output APK. The client of this engine should use this information to determine
+ * how the removal of this input APK's JAR entry affects the output APK.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
+ throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the output. It's safe
+ * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ void outputJarEntryRemoved(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that all JAR entries have been output.
+ *
+ * @return request to add JAR signature to the output or {@code null} if there is no need to add
+ * a JAR signature. The request will contain additional JAR entries to be output. The
+ * request must be fulfilled before
+ * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
+ *
+ * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
+ * from producing a valid signature. For example, if the engine uses the provided
+ * {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated 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 a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries, or if the engine is closed
+ */
+ OutputJarSignatureRequest outputJarEntries()
+ throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ *
The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
+ * DataSource)}.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated 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 a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ @Deprecated
+ OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ *
The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated 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 a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ OutputApkSigningBlockRequest2 outputZipSections2(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the signed APK was output.
+ *
+ *
This does not change the output APK. The method helps the client confirm that the current
+ * output is signed.
+ *
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output signatures, or if the engine is closed
+ */
+ void outputDone() throws IllegalStateException;
+
+ /**
+ * Generates a V4 signature proto and write to output file.
+ *
+ * @param data Input data to calculate a verity hash tree and hash root
+ * @param outputFile To store the serialized V4 Signature.
+ * @param ignoreFailures Whether any failures will be silently ignored.
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IOException if protobuf fails to be serialized and written to file
+ */
+ void signV4(DataSource data, File outputFile, boolean ignoreFailures)
+ throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
+
+ /**
+ * Checks if the signing configuration provided to the engine is capable of creating a
+ * SourceStamp.
+ */
+ default boolean isEligibleForSourceStamp() {
+ return false;
+ }
+
+ /** Generates the digest of the certificate used to sign the source stamp. */
+ default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ return new byte[0];
+ }
+
+ /**
+ * Indicates to this engine that it will no longer be used. Invoking this on an already closed
+ * engine is OK.
+ *
+ *
This does not change the output APK. For example, if the output APK is not yet fully
+ * signed, it will remain so after this method terminates.
+ */
+ @Override
+ void close();
+
+ /**
+ * Instructions about how to handle an input APK's JAR entry.
+ *
+ *
The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
+ * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
+ * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
+ * invoked.
+ */
+ public static class InputJarEntryInstructions {
+ private final OutputPolicy mOutputPolicy;
+ private final InspectJarEntryRequest mInspectJarEntryRequest;
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output policy and without a request to inspect the entry.
+ */
+ public InputJarEntryInstructions(OutputPolicy outputPolicy) {
+ this(outputPolicy, null);
+ }
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output mode and with the provided request to inspect the entry.
+ *
+ * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
+ * need to inspect the entry.
+ */
+ public InputJarEntryInstructions(
+ OutputPolicy outputPolicy,
+ InspectJarEntryRequest inspectJarEntryRequest) {
+ mOutputPolicy = outputPolicy;
+ mInspectJarEntryRequest = inspectJarEntryRequest;
+ }
+
+ /**
+ * Returns the output policy for this entry.
+ */
+ public OutputPolicy getOutputPolicy() {
+ return mOutputPolicy;
+ }
+
+ /**
+ * Returns the request to inspect the JAR entry or {@code null} if there is no need to
+ * inspect the entry.
+ */
+ public InspectJarEntryRequest getInspectJarEntryRequest() {
+ return mInspectJarEntryRequest;
+ }
+
+ /**
+ * Output policy for an input APK's JAR entry.
+ */
+ public static enum OutputPolicy {
+ /** Entry must not be output. */
+ SKIP,
+
+ /** Entry should be output. */
+ OUTPUT,
+
+ /** Entry will be output by the engine. The client can thus ignore this input entry. */
+ OUTPUT_BY_ENGINE,
+ }
+ }
+
+ /**
+ * Request to inspect the specified JAR entry.
+ *
+ *
The entry's uncompressed data must be provided to the data sink returned by
+ * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
+ * must be invoked.
+ */
+ interface InspectJarEntryRequest {
+
+ /**
+ * Returns the data sink into which the entry's uncompressed data should be sent.
+ */
+ DataSink getDataSink();
+
+ /**
+ * Indicates that entry's data has been provided in full.
+ */
+ void done();
+
+ /**
+ * Returns the name of the JAR entry.
+ */
+ String getEntryName();
+ }
+
+ /**
+ * Request to add JAR signature (aka v1 signature) to the output APK.
+ *
+ *
Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
+ * which {@link #done()} must be invoked.
+ */
+ interface OutputJarSignatureRequest {
+
+ /**
+ * Returns JAR entries that must be added to the output APK.
+ */
+ List getAdditionalJarEntries();
+
+ /**
+ * Indicates that the JAR entries contained in this request were added to the output APK.
+ */
+ void done();
+
+ /**
+ * JAR entry.
+ */
+ public static class JarEntry {
+ private final String mName;
+ private final byte[] mData;
+
+ /**
+ * Constructs a new {@code JarEntry} with the provided name and data.
+ *
+ * @param data uncompressed data of the entry. Changes to this array will not be
+ * reflected in {@link #getData()}.
+ */
+ public JarEntry(String name, byte[] data) {
+ mName = name;
+ mData = data.clone();
+ }
+
+ /**
+ * Returns the name of this ZIP entry.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the uncompressed data of this JAR entry.
+ */
+ public byte[] getData() {
+ return mData.clone();
+ }
+ }
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ * The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory, the offset of
+ * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
+ * accordingly, and then {@link #done()} must be invoked.
+ *
+ *
If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ *
+ * @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
+ */
+ @Deprecated
+ interface OutputApkSigningBlockRequest {
+
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ *
The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory. Immediately
+ * before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
+ * {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
+ * ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
+ * must be invoked.
+ *
+ *
If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ */
+ interface OutputApkSigningBlockRequest2 {
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+
+ /**
+ * Returns the number of 0x00 bytes the caller must place immediately before APK Signing
+ * Block.
+ */
+ int getPaddingSizeBeforeApkSigningBlock();
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/ApkVerificationIssue.java b/app/src/main/java/com/android/apksig/ApkVerificationIssue.java
new file mode 100644
index 00000000..fa2b7aa5
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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;
+
+/**
+ * This class is intended as a lightweight representation of an APK signature verification issue
+ * where the client does not require the additional textual details provided by a subclass.
+ */
+public class ApkVerificationIssue {
+ /* The V2 signer(s) could not be read from the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNERS = 1;
+ /* A V2 signature block exists without any V2 signers */
+ public static final int V2_SIG_NO_SIGNERS = 2;
+ /* Failed to parse a signer's block in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNER = 3;
+ /* Failed to parse the signer's signature record in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
+ /* The V2 signer contained no signatures */
+ public static final int V2_SIG_NO_SIGNATURES = 5;
+ /* The V2 signer's certificate could not be parsed */
+ public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
+ /* No signing certificates exist for the V2 signer */
+ public static final int V2_SIG_NO_CERTIFICATES = 7;
+ /* Failed to parse the V2 signer's digest record */
+ public static final int V2_SIG_MALFORMED_DIGEST = 8;
+ /* The V3 signer(s) could not be read from the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNERS = 9;
+ /* A V3 signature block exists without any V3 signers */
+ public static final int V3_SIG_NO_SIGNERS = 10;
+ /* Failed to parse a signer's block in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNER = 11;
+ /* Failed to parse the signer's signature record in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
+ /* The V3 signer contained no signatures */
+ public static final int V3_SIG_NO_SIGNATURES = 13;
+ /* The V3 signer's certificate could not be parsed */
+ public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
+ /* No signing certificates exist for the V3 signer */
+ public static final int V3_SIG_NO_CERTIFICATES = 15;
+ /* Failed to parse the V3 signer's digest record */
+ public static final int V3_SIG_MALFORMED_DIGEST = 16;
+ /* The source stamp signer contained no signatures */
+ public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
+ /* The source stamp signer's certificate could not be parsed */
+ public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
+ /* The source stamp contains a signature produced using an unknown algorithm */
+ public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
+ /* Failed to parse the signer's signature in the source stamp signature block */
+ public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
+ /* The source stamp's signature block failed verification */
+ public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
+ /* An exception was encountered when verifying the source stamp */
+ public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
+ /* The certificate digest in the APK does not match the expected digest */
+ public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
+ /*
+ * The APK contains a source stamp signature block without a corresponding stamp certificate
+ * digest in the APK contents.
+ */
+ public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
+ /*
+ * The APK does not contain the source stamp certificate digest file nor the source stamp
+ * signature block.
+ */
+ public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
+ /*
+ * None of the signatures provided by the source stamp were produced with a known signature
+ * algorithm.
+ */
+ public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
+ /*
+ * The source stamp signer's certificate in the signing block does not match the certificate in
+ * the APK.
+ */
+ public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
+ /* The APK could not be properly parsed due to a ZIP or APK format exception */
+ public static final int MALFORMED_APK = 28;
+ /* An unexpected exception was caught when attempting to verify the APK's signatures */
+ public static final int UNEXPECTED_EXCEPTION = 29;
+ /* The APK contains the certificate digest file but does not contain a stamp signature block */
+ public static final int SOURCE_STAMP_SIG_MISSING = 30;
+ /* Source stamp block contains a malformed attribute. */
+ public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
+ /* Source stamp block contains an unknown attribute. */
+ public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+ /** No V1 / jar signing signature blocks were found in the APK. */
+ public static final int JAR_SIG_NO_SIGNATURES = 36;
+ /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+ public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+ /** The source stamp timestamp attribute has an invalid value. */
+ public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
+
+ private final int mIssueId;
+ private final String mFormat;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
+ * {@code params}.
+ */
+ public ApkVerificationIssue(String format, Object... params) {
+ mIssueId = -1;
+ mFormat = format;
+ mParams = params;
+ }
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
+ * params}.
+ */
+ public ApkVerificationIssue(int issueId, Object... params) {
+ mIssueId = issueId;
+ mFormat = null;
+ mParams = params;
+ }
+
+ /**
+ * Returns the numeric ID for this issue.
+ */
+ public int getIssueId() {
+ return mIssueId;
+ }
+
+ /**
+ * Returns the optional parameters for this issue.
+ */
+ public Object[] getParams() {
+ return mParams;
+ }
+
+ @Override
+ public String toString() {
+ // If this instance was created by a subclass with a format string then return the same
+ // formatted String as the subclass.
+ if (mFormat != null) {
+ return String.format(mFormat, mParams);
+ }
+ StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
+ for (Object param : mParams) {
+ result.append(", ").append(param.toString());
+ }
+ return result.toString();
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/ApkVerifier.java b/app/src/main/java/com/android/apksig/ApkVerifier.java
new file mode 100644
index 00000000..9c4b56bc
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkVerifier.java
@@ -0,0 +1,3352 @@
+/*
+ * 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 static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest;
+import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
+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.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK signature verifier which mimics the behavior of the Android platform.
+ *
+ *
The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
+ * the verifier to be used for checking whether an APK's signatures are expected to verify on
+ * Android.
+ *
+ *
Use {@link Builder} to obtain instances of this verifier.
+ *
+ * @see Application Signing
+ */
+public class ApkVerifier {
+
+ private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES =
+ loadSupportedApkSigSchemeNames();
+
+ private static Map loadSupportedApkSigSchemeNames() {
+ Map supportedMap = new HashMap<>(2);
+ supportedMap.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2");
+ supportedMap.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3");
+ return supportedMap;
+ }
+
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+ private final File mV4SignatureFile;
+
+ private final Integer mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private ApkVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ File v4SignatureFile,
+ Integer minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mV4SignatureFile = v4SignatureFile;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers such
+ * as their signing certificates.
+ *
+ * Verification succeeds iff the APK's signature is expected to verify on all Android
+ * platform versions specified via the {@link Builder}. If the APK's signature is expected to
+ * not verify on any of the specified platform versions, this method returns a result with one
+ * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method
+ * throws an exception.
+ *
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws IllegalStateException if this verifier's configuration is missing required
+ * information.
+ */
+ public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ IllegalStateException {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verify(apk);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers.
+ *
+ * @param apk APK file contents
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ */
+ private Result verify(DataSource apk)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ int maxSdkVersion = mMaxSdkVersion;
+
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+
+ ByteBuffer androidManifest = null;
+
+ int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+ Result result = new Result();
+ Map> signatureSchemeApkContentDigests =
+ new HashMap<>();
+
+ // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
+ // name, but the verifiers use this parameter as the schemes supported by the target SDK
+ // range. Since the code below skips signature verification based on max SDK the mapping of
+ // supported schemes needs to be modified to ensure the verifiers do not report a stripped
+ // signature for an SDK range that does not support that signature version. For instance an
+ // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature
+ // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2
+ // verification is performed it would see the stripping protection attribute, see that V3
+ // is in the list of supported signatures, and report a stripped signature.
+ Map supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion);
+
+ // Android N and newer attempts to verify APKs using the APK Signing Block, which can
+ // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature
+ // verification. If the signature is found but does not verify, the APK is rejected.
+ Set foundApkSigSchemeIds = new HashSet<>(2);
+ if (maxSdkVersion >= AndroidSdkVersion.N) {
+ RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
+ // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+ // also includes stripping protection for the minimum SDK version on which the rotated
+ // signing key should be used.
+ int rotationMinSdkVersion = 0;
+ if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
+ try {
+ ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .build()
+ .verify();
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ rotationMinSdkVersion = v31Result.signers.isEmpty() ? 0 : Integer.MAX_VALUE;
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : v31Result.signers) {
+ if (rotationMinSdkVersion > signerInfo.minSdkVersion) {
+ rotationMinSdkVersion = signerInfo.minSdkVersion;
+ }
+ }
+ result.mergeFrom(v31Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31,
+ getApkContentDigestsFromSigningSchemeResult(v31Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3.1 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+ // Android P and newer attempts to verify APKs using APK Signature Scheme v3
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) {
+ try {
+ V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (rotationMinSdkVersion > 0) {
+ builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+ }
+ ApkSigningBlockUtils.Result v3Result = builder.build().verify();
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ result.mergeFrom(v3Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+ getApkContentDigestsFromSigningSchemeResult(v3Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3 signature not required unless a v3.1 signature was found as a v3.1
+ // signature is intended to support key rotation on T+ with the v3 signature
+ // containing the original signing key.
+ if (foundApkSigSchemeIds.contains(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P
+ // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or
+ // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if
+ // no APK Signature Scheme v3 (or newer scheme) signatures were found.
+ if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
+ try {
+ ApkSigningBlockUtils.Result v2Result =
+ V2SchemeVerifier.verify(
+ executor,
+ apk,
+ zipSections,
+ supportedSchemeNames,
+ foundApkSigSchemeIds,
+ Math.max(minSdkVersion, AndroidSdkVersion.N),
+ maxSdkVersion);
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ result.mergeFrom(v2Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ getApkContentDigestsFromSigningSchemeResult(v2Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v2 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // If v4 file is specified, use additional verification on it
+ if (mV4SignatureFile != null) {
+ final ApkSigningBlockUtils.Result v4Result =
+ V4SchemeVerifier.verify(apk, mV4SignatureFile);
+ foundApkSigSchemeIds.add(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ result.mergeFrom(v4Result);
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+ }
+
+ // Android O and newer requires that APKs targeting security sandbox version 2 and higher
+ // are signed using APK Signature Scheme v2 or newer.
+ if (maxSdkVersion >= AndroidSdkVersion.O) {
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int targetSandboxVersion =
+ getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (targetSandboxVersion > 1) {
+ if (foundApkSigSchemeIds.isEmpty()) {
+ result.addError(
+ Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION,
+ targetSandboxVersion);
+ }
+ }
+ }
+
+ List cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
+ // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
+ // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
+ // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
+ // scheme) signatures were found.
+ if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
+ V1SchemeVerifier.Result v1Result =
+ V1SchemeVerifier.verify(
+ apk,
+ zipSections,
+ supportedSchemeNames,
+ foundApkSigSchemeIds,
+ minSdkVersion,
+ maxSdkVersion);
+ result.mergeFrom(v1Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verify the SourceStamp, if found in the APK.
+ try {
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+ cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+ // If SourceStamp file is found inside the APK, there must be a SourceStamp
+ // block in the APK signing block as well.
+ if (sourceStampCdRecord != null) {
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ Math.max(minSdkVersion, AndroidSdkVersion.R),
+ maxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ }
+ } catch (SignatureNotFoundException ignored) {
+ result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2
+ // signatures verified.
+ if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) {
+ ArrayList v1Signers =
+ new ArrayList<>(result.getV1SchemeSigners());
+ ArrayList v2Signers =
+ new ArrayList<>(result.getV2SchemeSigners());
+ ArrayList v1SignerCerts = new ArrayList<>();
+ ArrayList v2SignerCerts = new ArrayList<>();
+ for (Result.V1SchemeSignerInfo signer : v1Signers) {
+ try {
+ v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+ } catch (CertificateEncodingException e) {
+ throw new IllegalStateException(
+ "Failed to encode JAR signer " + signer.getName() + " certs", e);
+ }
+ }
+ for (Result.V2SchemeSignerInfo signer : v2Signers) {
+ try {
+ v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+ } catch (CertificateEncodingException e) {
+ throw new IllegalStateException(
+ "Failed to encode APK Signature Scheme v2 signer (index: "
+ + signer.getIndex() + ") certs",
+ e);
+ }
+ }
+
+ for (int i = 0; i < v1SignerCerts.size(); i++) {
+ ByteArray v1Cert = v1SignerCerts.get(i);
+ if (!v2SignerCerts.contains(v1Cert)) {
+ Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i);
+ v1Signer.addError(Issue.V2_SIG_MISSING);
+ break;
+ }
+ }
+ for (int i = 0; i < v2SignerCerts.size(); i++) {
+ ByteArray v2Cert = v2SignerCerts.get(i);
+ if (!v1SignerCerts.contains(v2Cert)) {
+ Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i);
+ v2Signer.addError(Issue.JAR_SIG_MISSING);
+ break;
+ }
+ }
+ }
+
+ // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a
+ // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer
+ // matches the oldest signing certificate in the provided SigningCertificateLineage
+ if (result.isVerifiedUsingV3Scheme()
+ && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) {
+ SigningCertificateLineage lineage = result.getSigningCertificateLineage();
+ X509Certificate oldSignerCert;
+ if (result.isVerifiedUsingV1Scheme()) {
+ List v1Signers = result.getV1SchemeSigners();
+ if (v1Signers.size() != 1) {
+ // APK Signature Scheme v3 only supports single-signers, error to sign with
+ // multiple and then only one
+ result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+ }
+ oldSignerCert = v1Signers.get(0).mCertChain.get(0);
+ } else {
+ List v2Signers = result.getV2SchemeSigners();
+ if (v2Signers.size() != 1) {
+ // APK Signature Scheme v3 only supports single-signers, error to sign with
+ // multiple and then only one
+ result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+ }
+ oldSignerCert = v2Signers.get(0).mCerts.get(0);
+ }
+ if (lineage == null) {
+ // no signing certificate history with which to contend, just make sure that v3
+ // matches previous versions
+ List v3Signers = result.getV3SchemeSigners();
+ if (v3Signers.size() != 1) {
+ // multiple v3 signers should never exist without rotation history, since
+ // multiple signers implies a different signer for different platform versions
+ result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS);
+ }
+ try {
+ if (!Arrays.equals(oldSignerCert.getEncoded(),
+ v3Signers.get(0).mCerts.get(0).getEncoded())) {
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ } catch (CertificateEncodingException e) {
+ // we just go the encoding for the v1/v2 certs above, so must be v3
+ throw new RuntimeException(
+ "Failed to encode APK Signature Scheme v3 signer cert", e);
+ }
+ } else {
+ // we have some signing history, make sure that the root of the history is the same
+ // as our v1/v2 signer
+ try {
+ lineage = lineage.getSubLineage(oldSignerCert);
+ if (lineage.size() != 1) {
+ // the v1/v2 signer was found, but not at the root of the lineage
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ } catch (IllegalArgumentException e) {
+ // the v1/v2 signer was not found in the lineage
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ }
+ }
+
+
+ // If there is a v4 scheme signer, make sure that their certificates match.
+ // The apkDigest field in the v4 signature should match the selected v2/v3.
+ if (result.isVerifiedUsingV4Scheme()) {
+ List v4Signers = result.getV4SchemeSigners();
+
+ List digestsFromV4 =
+ v4Signers.get(0).getContentDigests();
+ if (digestsFromV4.size() != 1) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1;
+ if (v4Signers.size() != expectedSize) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
+ result);
+ if (result.isVerifiedUsingV31Scheme()) {
+ checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
+ digestFromV4, result);
+ }
+ } else if (result.isVerifiedUsingV2Scheme()) {
+ if (v4Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ List v2Signers = result.getV2SchemeSigners();
+ if (v2Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV2 = pickBestDigestForV4(
+ v2Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV2)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ } else {
+ throw new RuntimeException("V4 signature must be also verified with V2/V3");
+ }
+ }
+
+ // If the targetSdkVersion has a minimum required signature scheme version then verify
+ // that the APK was signed with at least that version.
+ try {
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ } catch (ApkFormatException e) {
+ // If the manifest is not available then skip the minimum signature scheme requirement
+ // to support bundle verification.
+ }
+ if (androidManifest != null) {
+ int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+ androidManifest.slice());
+ int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+ // The platform currently only enforces a single minimum signature scheme version, but
+ // when later platform versions support another minimum version this will need to be
+ // expanded to verify the minimum based on the target and maximum SDK version.
+ if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME
+ && maxSdkVersion >= targetSdkVersion) {
+ switch (minSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ if (result.isVerifiedUsingV2Scheme()) {
+ break;
+ }
+ // Allow this case to fall through to the next as a signature satisfying a
+ // later scheme version will also satisfy this requirement.
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) {
+ break;
+ }
+ result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+ targetSdkVersion,
+ minSchemeVersion);
+ }
+ }
+ }
+
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verified
+ result.setVerified();
+ if (result.isVerifiedUsingV31Scheme()) {
+ List v31Signers = result.getV31SchemeSigners();
+ result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV3Scheme()) {
+ List v3Signers = result.getV3SchemeSigners();
+ result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV2Scheme()) {
+ for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+ result.addSignerCertificate(signerInfo.getCertificate());
+ }
+ } else if (result.isVerifiedUsingV1Scheme()) {
+ for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) {
+ result.addSignerCertificate(signerInfo.getCertificate());
+ }
+ } else {
+ throw new RuntimeException(
+ "APK verified, but has not verified using any of v1, v2 or v3 schemes");
+ }
+
+ return result;
+ }
+
+ /**
+ * Verifies and returns the minimum SDK version, either as provided to the builder or as read
+ * from the {@code apk}'s AndroidManifest.xml.
+ */
+ private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws ApkFormatException, IOException {
+ if (mMinSdkVersion != null) {
+ if (mMinSdkVersion < 0) {
+ throw new IllegalArgumentException(
+ "minSdkVersion must not be negative: " + mMinSdkVersion);
+ }
+ if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
+ throw new IllegalArgumentException(
+ "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
+ + ")");
+ }
+ return mMinSdkVersion;
+ }
+
+ ByteBuffer androidManifest = null;
+ // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int minSdkVersion =
+ ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (minSdkVersion > mMaxSdkVersion) {
+ throw new IllegalArgumentException(
+ "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+ + mMaxSdkVersion + ")");
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the mapping of signature scheme version to signature scheme name for all signature
+ * schemes starting from V2 supported by the {@code maxSdkVersion}.
+ */
+ private static Map getSupportedSchemeNames(int maxSdkVersion) {
+ Map supportedSchemeNames;
+ if (maxSdkVersion >= AndroidSdkVersion.P) {
+ supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
+ } else if (maxSdkVersion >= AndroidSdkVersion.N) {
+ supportedSchemeNames = new HashMap<>(1);
+ supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ SUPPORTED_APK_SIG_SCHEME_NAMES.get(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ } else {
+ supportedSchemeNames = Collections.emptyMap();
+ }
+ return supportedSchemeNames;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ * The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified} returns {@code true}. The details of the source stamp verification can
+ * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or
+ * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the
+ * verification fails additional details regarding the failure can be obtained from {@link
+ * Result#getAllErrors()}}.
+ */
+ public Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ *
A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+ * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+ * returns the result of the verification.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private Result verifySourceStamp(DataSource apk, String expectedCertDigest) {
+ try {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
+ stampSigningBlockFound = true;
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ if (stampSigningBlockFound) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ } else {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ }
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus
+ .CERT_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest,
+ expectedCertDigest);
+ }
+ }
+
+ Map> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ Map supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion);
+ Set foundApkSigSchemeIds = new HashSet<>(2);
+
+ Result result = new Result();
+ ApkSigningBlockUtils.Result v3Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ Math.max(minSdkVersion, AndroidSdkVersion.P));
+ if (v3Result != null && v3Result.containsErrors()) {
+ result.mergeFrom(v3Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ ApkSigningBlockUtils.Result v2Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P
+ || foundApkSigSchemeIds.isEmpty())) {
+ v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ Math.max(minSdkVersion, AndroidSdkVersion.N));
+ if (v2Result != null && v2Result.containsErrors()) {
+ result.mergeFrom(v2Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) {
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ // Since the caller is only seeking to verify the source stamp the Result can be marked
+ // as verified if the source stamp verification was successful.
+ if (sourceStampResult.verified) {
+ result.setVerified();
+ } else {
+ // To prevent APK signature verification with a failed / missing source stamp the
+ // source stamp verification will only log warnings; to allow the caller to capture
+ // the failure reason treat all warnings as errors.
+ result.setWarningsAsErrors(true);
+ }
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+ }
+
+ /**
+ * Creates and returns a {@code Result} that can be returned for source stamp verification
+ * with the provided source stamp {@code verificationStatus}, and logs an error for the
+ * specified {@code issue} and {@code params}.
+ */
+ private static Result createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue,
+ Object... params) {
+ Result result = new Result();
+ result.addError(issue, params);
+ return mergeSourceStampResult(verificationStatus, result);
+ }
+
+ /**
+ * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the
+ * source stamp status to the provided {@code verificationStatus}.
+ */
+ private static Result mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus,
+ Result result) {
+ result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus);
+ return result;
+ }
+
+ /**
+ * Obtains the APK content digest(s) and adds them to the provided {@code
+ * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+ * merged with a {@code Result} to notify the client of any errors.
+ *
+ * Note, this method currently only supports signature scheme V2 and V3; to obtain the
+ * content digests for V1 signatures use {@link
+ * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+ * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+ * returned.
+ */
+ private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+ ApkUtils.ZipSections zipSections, Set foundApkSigSchemeIds,
+ Map supportedSchemeNames,
+ Map> sigSchemeApkContentDigests,
+ int apkSigSchemeVersion, int minSdkVersion)
+ throws IOException, NoSuchAlgorithmException {
+ if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
+ || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) {
+ return null;
+ }
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion);
+ SignatureInfo signatureInfo;
+ try {
+ int sigSchemeBlockId = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
+ ? V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID
+ : V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+ signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
+ sigSchemeBlockId, result);
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ return null;
+ }
+ foundApkSigSchemeIds.add(apkSigSchemeVersion);
+
+ Set contentDigestsToVerify = new HashSet<>(1);
+ if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+ V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, supportedSchemeNames,
+ foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result);
+ } else {
+ V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, result);
+ }
+ Map apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+ signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+ contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(),
+ contentDigest.getValue());
+ }
+ }
+ sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests);
+ return result;
+ }
+
+ private static void checkV4Signer(List v3Signers,
+ List v4Certs, byte[] digestFromV4, Result result) {
+ if (v3Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV3)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ }
+
+ private static void checkV4Certificate(List v4Certs,
+ List v2v3Certs, Result result) {
+ try {
+ byte[] v4Cert = v4Certs.get(0).getEncoded();
+ byte[] cert = v2v3Certs.get(0).getEncoded();
+ if (!Arrays.equals(cert, v4Cert)) {
+ result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH);
+ }
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException("Failed to encode APK signer cert", e);
+ }
+ }
+
+ private static byte[] pickBestDigestForV4(
+ List contentDigests) {
+ Map apkContentDigests = new HashMap<>();
+ collectApkContentDigests(contentDigests, apkContentDigests);
+ return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
+ }
+
+ private static Map getApkContentDigestsFromSigningSchemeResult(
+ ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+ Map apkContentDigests = new HashMap<>();
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+ collectApkContentDigests(signerInfo.contentDigests, apkContentDigests);
+ }
+ return apkContentDigests;
+ }
+
+ private static Map getApkContentDigestFromV1SigningScheme(
+ List cdRecords,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ Map v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ manifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ private static void collectApkContentDigests(
+ List contentDigests,
+ Map apkContentDigests) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+ }
+
+ }
+
+ private static ByteBuffer getAndroidManifestFromApk(
+ DataSource apk, ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ List cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ try {
+ return ApkSigner.getAndroidManifestFromApk(
+ cdRecords,
+ apk.slice(0, zipSections.getZipCentralDirectoryOffset()));
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read AndroidManifest.xml", e);
+ }
+ }
+
+ private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
+ if (targetSdkVersion >= AndroidSdkVersion.R) {
+ return VERSION_APK_SIGNATURE_SCHEME_V2;
+ }
+ return VERSION_JAR_SIGNATURE_SCHEME;
+ }
+
+ /**
+ * Result of verifying an APKs signatures. The APK can be considered verified iff
+ * {@link #isVerified()} returns {@code true}.
+ */
+ public static class Result {
+ private final List mErrors = new ArrayList<>();
+ private final List mWarnings = new ArrayList<>();
+ private final List mSignerCerts = new ArrayList<>();
+ private final List mV1SchemeSigners = new ArrayList<>();
+ private final List mV1SchemeIgnoredSigners = new ArrayList<>();
+ private final List mV2SchemeSigners = new ArrayList<>();
+ private final List mV3SchemeSigners = new ArrayList<>();
+ private final List mV31SchemeSigners = new ArrayList<>();
+ private final List mV4SchemeSigners = new ArrayList<>();
+ private SourceStampInfo mSourceStampInfo;
+
+ private boolean mVerified;
+ private boolean mVerifiedUsingV1Scheme;
+ private boolean mVerifiedUsingV2Scheme;
+ private boolean mVerifiedUsingV3Scheme;
+ private boolean mVerifiedUsingV31Scheme;
+ private boolean mVerifiedUsingV4Scheme;
+ private boolean mSourceStampVerified;
+ private boolean mWarningsAsErrors;
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ /**
+ * Returns {@code true} if the APK's signatures verified.
+ */
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ private void setVerified() {
+ mVerified = true;
+ }
+
+ /**
+ * Returns {@code true} if the APK's JAR signatures verified.
+ */
+ public boolean isVerifiedUsingV1Scheme() {
+ return mVerifiedUsingV1Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
+ */
+ public boolean isVerifiedUsingV2Scheme() {
+ return mVerifiedUsingV2Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified.
+ */
+ public boolean isVerifiedUsingV3Scheme() {
+ return mVerifiedUsingV3Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+ */
+ public boolean isVerifiedUsingV31Scheme() {
+ return mVerifiedUsingV31Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
+ */
+ public boolean isVerifiedUsingV4Scheme() {
+ return mVerifiedUsingV4Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's SourceStamp signature verified.
+ */
+ public boolean isSourceStampVerified() {
+ return mSourceStampVerified;
+ }
+
+ /**
+ * Returns the verified signers' certificates, one per signer.
+ */
+ public List getSignerCertificates() {
+ return mSignerCerts;
+ }
+
+ private void addSignerCertificate(X509Certificate cert) {
+ mSignerCerts.add(cert);
+ }
+
+ /**
+ * Returns information about JAR signers associated with the APK's signature. These are the
+ * signers used by Android.
+ *
+ * @see #getV1SchemeIgnoredSigners()
+ */
+ public List getV1SchemeSigners() {
+ return mV1SchemeSigners;
+ }
+
+ /**
+ * Returns information about JAR signers ignored by the APK's signature verification
+ * process. These signers are ignored by Android. However, each signer's errors or warnings
+ * will contain information about why they are ignored.
+ *
+ * @see #getV1SchemeSigners()
+ */
+ public List getV1SchemeIgnoredSigners() {
+ return mV1SchemeIgnoredSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v2 signers associated with the APK's
+ * signature.
+ */
+ public List getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v3 signers associated with the APK's
+ * signature.
+ *
+ * Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3 only supports single
+ * signer identities.
+ */
+ public List getV3SchemeSigners() {
+ return mV3SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+ * signature.
+ *
+ * Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single
+ * signer identities.
+ */
+ public List getV31SchemeSigners() {
+ return mV31SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v4 signers associated with the APK's
+ * signature.
+ */
+ public List getV4SchemeSigners() {
+ return mV4SchemeSigners;
+ }
+
+ /**
+ * Returns information about SourceStamp associated with the APK's signature.
+ */
+ public SourceStampInfo getSourceStampInfo() {
+ return mSourceStampInfo;
+ }
+
+ /**
+ * Returns the combined SigningCertificateLineage associated with this APK's APK Signature
+ * Scheme v3 signing block.
+ */
+ public SigningCertificateLineage getSigningCertificateLineage() {
+ return mSigningCertificateLineage;
+ }
+
+ void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
+ /**
+ * Sets whether warnings should be treated as errors.
+ */
+ void setWarningsAsErrors(boolean value) {
+ mWarningsAsErrors = value;
+ }
+
+ /**
+ * Returns errors encountered while verifying the APK's signatures.
+ */
+ public List getErrors() {
+ if (!mWarningsAsErrors) {
+ return mErrors;
+ } else {
+ List allErrors = new ArrayList<>();
+ allErrors.addAll(mErrors);
+ allErrors.addAll(mWarnings);
+ return allErrors;
+ }
+ }
+
+ /**
+ * Returns warnings encountered while verifying the APK's signatures.
+ */
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ private void mergeFrom(V1SchemeVerifier.Result source) {
+ mVerifiedUsingV1Scheme = source.verified;
+ mErrors.addAll(source.getErrors());
+ mWarnings.addAll(source.getWarnings());
+ for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) {
+ mV1SchemeSigners.add(new V1SchemeSignerInfo(signer));
+ }
+ for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) {
+ mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer));
+ }
+ }
+
+ private void mergeFrom(ApkSigResult source) {
+ switch (source.signatureSchemeVersion) {
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.mSigners.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown ApkSigResult Signing Block Scheme Id "
+ + source.signatureSchemeVersion);
+ }
+ }
+
+ private void mergeFrom(ApkSigningBlockUtils.Result source) {
+ switch (source.signatureSchemeVersion) {
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+ mVerifiedUsingV2Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+ mVerifiedUsingV3Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
+ // Do not overwrite a previously set lineage from a v3.1 signing block.
+ if (mSigningCertificateLineage == null) {
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31:
+ mVerifiedUsingV31Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+ mVerifiedUsingV4Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.signers.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
+ }
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying the APK. Any error
+ * prevents the APK from being considered verified.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ if (mSourceStampInfo.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns all errors for this result, including any errors from signature scheme signers
+ * and the source stamp.
+ */
+ public List getAllErrors() {
+ List errors = new ArrayList<>();
+ errors.addAll(mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(mWarnings);
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ errors.addAll(mSourceStampInfo.getErrors());
+ if (mWarningsAsErrors) {
+ errors.addAll(mSourceStampInfo.getWarnings());
+ }
+ }
+ return errors;
+ }
+
+ /**
+ * Information about a JAR signer associated with the APK's signature.
+ */
+ public static class V1SchemeSignerInfo {
+ private final String mName;
+ private final List mCertChain;
+ private final String mSignatureBlockFileName;
+ private final String mSignatureFileName;
+
+ private final List mErrors;
+ private final List mWarnings;
+
+ private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) {
+ mName = result.name;
+ mCertChain = result.certChain;
+ mSignatureBlockFileName = result.signatureBlockFileName;
+ mSignatureFileName = result.signatureFileName;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns a user-friendly name of the signer.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the name of the JAR entry containing this signer's JAR signature block file.
+ */
+ public String getSignatureBlockFileName() {
+ return mSignatureBlockFileName;
+ }
+
+ /**
+ * Returns the name of the JAR entry containing this signer's JAR signature file.
+ */
+ public String getSignatureFileName() {
+ return mSignatureFileName;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertChain.isEmpty() ? null : mCertChain.get(0);
+ }
+
+ /**
+ * Returns the certificate chain for the signer's public key. The certificate containing
+ * the public key is first, followed by the certificate (if any) which issued the
+ * signing certificate, and so forth. An empty list may be returned if an error was
+ * encountered during verification (see {@link #containsErrors()}).
+ */
+ public List getCertificateChain() {
+ return mCertChain;
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying this signer's JAR
+ * signature. Any error prevents the signer's signature from being considered verified.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ /**
+ * Returns errors encountered while verifying this signer's JAR signature. Any error
+ * prevents the signer's signature from being considered verified.
+ */
+ public List getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns warnings encountered while verifying this signer's JAR signature. Warnings
+ * do not prevent the signer's signature from being considered verified.
+ */
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ private void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
+ */
+ public static class V2SchemeSignerInfo {
+ private final int mIndex;
+ private final List mCerts;
+
+ private final List mErrors;
+ private final List mWarnings;
+ private final List
+ mContentDigests;
+
+ private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v2 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List getCertificates() {
+ return mCerts;
+ }
+
+ private void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ public List getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme v3 signer associated with the APK's signature.
+ */
+ public static class V3SchemeSignerInfo {
+ private final int mIndex;
+ private final List mCerts;
+
+ private final List mErrors;
+ private final List mWarnings;
+ private final List
+ mContentDigests;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+
+ private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ mMinSdkVersion = result.minSdkVersion;
+ mMaxSdkVersion = result.maxSdkVersion;
+ boolean rotationTargetsDevRelease = false;
+ for (ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute attribute : result.additionalAttributes) {
+ if (attribute.getId() == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
+ rotationTargetsDevRelease = true;
+ break;
+ }
+ }
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v3 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ public List getContentDigests() {
+ return mContentDigests;
+ }
+
+ /**
+ * Returns the minimum SDK version on which this signer should be verified.
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /**
+ * Returns the maximum SDK version on which this signer should be verified.
+ */
+ public int getMaxSdkVersion() {
+ return mMaxSdkVersion;
+ }
+
+ /**
+ * Returns whether rotation is targeting a development release.
+ *
+ * A development release uses the SDK version of the previously released platform
+ * until the SDK of the development release is finalized. To allow rotation to target
+ * a development release after T, this attribute must be set to ensure rotation is
+ * used on the development release but ignored on the released platform with the same
+ * API level.
+ */
+ public boolean getRotationTargetsDevRelease() {
+ return mRotationTargetsDevRelease;
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme V4 signer associated with the APK's
+ * signature.
+ */
+ public static class V4SchemeSignerInfo {
+ private final int mIndex;
+ private final List mCerts;
+
+ private final List mErrors;
+ private final List mWarnings;
+ private final List
+ mContentDigests;
+
+ private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v3 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ public List getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * Information about SourceStamp associated with the APK's signature.
+ */
+ public static class SourceStampInfo {
+ public enum SourceStampVerificationStatus {
+ /** The stamp is present and was successfully verified. */
+ STAMP_VERIFIED,
+ /** The stamp is present but failed verification. */
+ STAMP_VERIFICATION_FAILED,
+ /** The expected cert digest did not match the digest in the APK. */
+ CERT_DIGEST_MISMATCH,
+ /** The stamp is not present at all. */
+ STAMP_MISSING,
+ /** The stamp is at least partially present, but was not able to be verified. */
+ STAMP_NOT_VERIFIED,
+ /** The stamp was not able to be verified due to an unexpected error. */
+ VERIFICATION_ERROR
+ }
+
+ private final List mCertificates;
+ private final List mCertificateLineage;
+
+ private final List mErrors;
+ private final List mWarnings;
+
+ private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+
+ private final long mTimestamp;
+
+ private SourceStampInfo(ApkSignerInfo result) {
+ mCertificates = result.certs;
+ mCertificateLineage = result.certificateLineage;
+ mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getErrors());
+ mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getWarnings());
+ if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+ mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+ } else {
+ mSourceStampVerificationStatus =
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+ }
+ mTimestamp = result.timestamp;
+ }
+
+ SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+ mCertificates = Collections.emptyList();
+ mCertificateLineage = Collections.emptyList();
+ mErrors = Collections.emptyList();
+ mWarnings = Collections.emptyList();
+ mSourceStampVerificationStatus = sourceStampVerificationStatus;
+ mTimestamp = 0;
+ }
+
+ /**
+ * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the SourceStamp's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificates.isEmpty() ? null : mCertificates.get(0);
+ }
+
+ /**
+ * Returns a list containing all of the certificates in the stamp certificate lineage.
+ */
+ public List getCertificatesInLineage() {
+ return mCertificateLineage;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns the reason for any source stamp verification failures, or {@code
+ * STAMP_VERIFIED} if the source stamp was successfully verified.
+ */
+ public SourceStampVerificationStatus getSourceStampVerificationStatus() {
+ return mSourceStampVerificationStatus;
+ }
+
+ /**
+ * Returns the epoch timestamp in seconds representing the time this source stamp block
+ * was signed, or 0 if the timestamp is not available.
+ */
+ public long getTimestampEpochSeconds() {
+ return mTimestamp;
+ }
+ }
+ }
+
+ /**
+ * Error or warning encountered while verifying an APK's signatures.
+ */
+ public enum Issue {
+
+ /**
+ * APK is not JAR-signed.
+ */
+ JAR_SIG_NO_SIGNATURES("No JAR signatures"),
+
+ /**
+ * APK does not contain any entries covered by JAR signatures.
+ */
+ JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"),
+
+ /**
+ * APK contains multiple entries with the same name.
+ *
+ *
+ * - Parameter 1: name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
+
+ /**
+ * JAR manifest contains a section with a duplicate name.
+ *
+ *
+ * - Parameter 1: section name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
+
+ /**
+ * JAR manifest contains a section without a name.
+ *
+ *
+ * - Parameter 1: section index (1-based) ({@code Integer})
+ *
+ */
+ JAR_SIG_UNNNAMED_MANIFEST_SECTION(
+ "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"),
+
+ /**
+ * JAR signature file contains a section without a name.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ * - Parameter 2: section index (1-based) ({@code Integer})
+ *
+ */
+ JAR_SIG_UNNNAMED_SIG_FILE_SECTION(
+ "Malformed %1$s: invidual section #%2$d does not have a name"),
+
+ /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */
+ JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"),
+
+ /**
+ * JAR manifest references an entry which is not there in the APK.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST(
+ "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"),
+
+ /**
+ * JAR manifest does not list a digest for the specified entry.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"),
+
+ /**
+ * JAR signature does not list a digest for the specified entry.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ * - Parameter 2: signature file name ({@code String})
+ *
+ */
+ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"),
+
+ /**
+ * The specified JAR entry is not covered by JAR signature.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"),
+
+ /**
+ * JAR signature uses different set of signers to protect the two specified ZIP entries.
+ *
+ *
+ * - Parameter 1: first entry name ({@code String})
+ * - Parameter 2: first entry signer names ({@code List})
+ * - Parameter 3: second entry name ({@code String})
+ * - Parameter 4: second entry signer names ({@code List})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH(
+ "Entries %1$s and %3$s are signed with different sets of signers"
+ + " : <%2$s> vs <%4$s>"),
+
+ /**
+ * Digest of the specified ZIP entry's data does not match the digest expected by the JAR
+ * signature.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ * - Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 3: name of the entry in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 4: base64-encoded actual digest ({@code String})
+ * - Parameter 5: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY(
+ "%2$s digest of %1$s does not match the digest specified in %3$s"
+ + ". Expected: <%5$s>, actual: <%4$s>"),
+
+ /**
+ * Digest of the JAR manifest main section did not verify.
+ *
+ *
+ * - Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 2: name of the entry in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 3: base64-encoded actual digest ({@code String})
+ * - Parameter 4: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY(
+ "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest"
+ + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"),
+
+ /**
+ * Digest of the specified JAR manifest section does not match the digest expected by the
+ * JAR signature.
+ *
+ *
+ * - Parameter 1: section name ({@code String})
+ * - Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 3: name of the signature file in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 4: base64-encoded actual digest ({@code String})
+ * - Parameter 5: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY(
+ "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest"
+ + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"),
+
+ /**
+ * JAR signature file does not contain the whole-file digest of the JAR manifest file. The
+ * digest speeds up verification of JAR signature.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE(
+ "%1$s does not specify digest of META-INF/MANIFEST.MF"
+ + ". This slows down verification."),
+
+ /**
+ * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not
+ * contain protections against stripping of these newer scheme signatures.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_NO_APK_SIG_STRIP_PROTECTION(
+ "APK is signed using APK Signature Scheme v2 but these signatures may be stripped"
+ + " without being detected because %1$s does not contain anti-stripping"
+ + " protections."),
+
+ /**
+ * JAR signature of the signer is missing a file/entry.
+ *
+ *
+ * - Parameter 1: name of the encountered file ({@code String})
+ * - Parameter 2: name of the missing file ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"),
+
+ /**
+ * An exception was encountered while verifying JAR signature contained in a signature block
+ * against the signature file.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: name of the signature file ({@code String})
+ * - Parameter 3: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
+
+ /**
+ * JAR signature contains unsupported digest algorithm.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: digest algorithm OID ({@code String})
+ * - Parameter 3: signature algorithm OID ({@code String})
+ * - Parameter 4: API Levels on which this combination of algorithms is not supported
+ * ({@code String})
+ * - Parameter 5: user-friendly variant of digest algorithm ({@code String})
+ * - Parameter 6: user-friendly variant of signature algorithm ({@code String})
+ *
+ */
+ JAR_SIG_UNSUPPORTED_SIG_ALG(
+ "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which"
+ + " is not supported on API Level(s) %4$s for which this APK is being"
+ + " verified"),
+
+ /**
+ * An exception was encountered while parsing JAR signature contained in a signature block.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"),
+
+ /**
+ * An exception was encountered while parsing a certificate contained in the JAR signature
+ * block.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"),
+
+ /**
+ * JAR signature contained in a signature block file did not verify against the signature
+ * file.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
+
+ /**
+ * JAR signature contains no verified signers.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ *
+ */
+ JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
+
+ /**
+ * JAR signature file contains a section with a duplicate name.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ * - Parameter 1: section name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"),
+
+ /**
+ * JAR signature file's main section doesn't contain the mandatory Signature-Version
+ * attribute.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE(
+ "Malformed %1$s: missing Signature-Version attribute"),
+
+ /**
+ * JAR signature file references an unknown APK signature scheme ID.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ * - Parameter 2: unknown APK signature scheme ID ({@code} Integer)
+ *
+ */
+ JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+ "JAR signature %1$s references unknown APK signature scheme ID: %2$d"),
+
+ /**
+ * JAR signature file indicates that the APK is supposed to be signed with a supported APK
+ * signature scheme (in addition to the JAR signature) but no such signature was found in
+ * the APK.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ * - Parameter 2: APK signature scheme ID ({@code} Integer)
+ * - Parameter 3: APK signature scheme English name ({@code} String)
+ *
+ */
+ JAR_SIG_MISSING_APK_SIG_REFERENCED(
+ "JAR signature %1$s indicates the APK is signed using %3$s but no such signature"
+ + " was found. Signature stripped?"),
+
+ /**
+ * JAR entry is not covered by signature and thus unauthorized modifications to its contents
+ * will not be detected.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_UNPROTECTED_ZIP_ENTRY(
+ "%1$s not protected by signature. Unauthorized modifications to this JAR entry"
+ + " will not be detected. Delete or move the entry outside of META-INF/."),
+
+ /**
+ * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK
+ * Signature Scheme v2 signature from this signer, but does not contain a JAR signature
+ * from this signer.
+ */
+ JAR_SIG_MISSING("No JAR signature from this signer"),
+
+ /**
+ * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but
+ * no such signature was found.
+ *
+ *
+ * - Parameter 1: target sandbox version ({@code Integer})
+ *
+ */
+ NO_SIG_FOR_TARGET_SANDBOX_VERSION(
+ "Missing APK Signature Scheme v2 signature required for target sandbox version"
+ + " %1$d"),
+
+ /**
+ * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+ * APK is not signed with that version or later.
+ *
+ *
+ * - Parameter 1: target SDK Version (@code Integer})
+ * - Parameter 2: minimum signature scheme version ((@code Integer})
+ *
+ */
+ MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+ "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+ + " not signed with this or a later signature scheme"),
+
+ /**
+ * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
+ * signature from this signer, but does not contain an APK Signature Scheme v2 signature
+ * from this signer.
+ */
+ V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"),
+
+ /**
+ * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
+ */
+ V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+ /**
+ * Failed to parse this signer's signer block contained in the APK Signature Scheme v2
+ * signature.
+ */
+ V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
+ * parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v2 signer's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})
+ * - Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})
+ * - Parameter 3: error details ({@code Throwable})
+ *
+ */
+ V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
+
+ /**
+ * Failed to parse this signer's digest record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+ *
+ *
+ * - Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v2 signature references an unknown APK signature scheme ID.
+ *
+ *
+ * - Parameter 1: signer index ({@code Integer})
+ * - Parameter 2: unknown APK signature scheme ID ({@code} Integer)
+ *
+ */
+ V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+ "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: "
+ + "%2$d"),
+
+ /**
+ * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a
+ * supported APK signature scheme (in addition to the v2 signature) but no such signature
+ * was found in the APK.
+ *
+ *
+ * - Parameter 1: signer index ({@code Integer})
+ * - Parameter 2: APK signature scheme English name ({@code} String)
+ *
+ */
+ V2_SIG_MISSING_APK_SIG_REFERENCED(
+ "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but "
+ + "no such signature was found. Signature stripped?"),
+
+ /**
+ * APK Signature Scheme v2 signature contains no signers.
+ */
+ V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a signature produced using an unknown
+ * algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+ *
+ *
+ * - Parameter 1: attribute ID ({@code Integer})
+ *
+ */
+ V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v2 signature of this
+ * signer.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no signatures.
+ */
+ V2_SIG_NO_SIGNATURES("No signatures"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
+ */
+ V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no certificates.
+ */
+ V2_SIG_NO_CERTIFICATES("No certificates"),
+
+ /**
+ * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
+ * not match the public key listed in the signatures record.
+ *
+ *
+ * - Parameter 1: hex-encoded public key from certificate ({@code String})
+ * - Parameter 2: hex-encoded public key from signatures record ({@code String})
+ *
+ */
+ V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
+ * record do not match the signature algorithms listed in the signatures record.
+ *
+ *
+ * - Parameter 1: signature algorithms from signatures record ({@code List})
+ * - Parameter 2: signature algorithms from digests record ({@code List})
+ *
+ */
+ V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+ "Signature algorithms mismatch between signatures and digests records"
+ + ": %1$s vs %2$s"),
+
+ /**
+ * The APK's digest does not match the digest contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected digest of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual digest of the APK ({@code String})
+ *
+ */
+ V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
+ "APK integrity check failed. %1$s digest mismatch."
+ + " Expected: <%2$s>, actual: <%3$s>"),
+
+ /**
+ * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature.
+ */
+ V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+ /**
+ * Failed to parse this signer's signer block contained in the APK Signature Scheme v3
+ * signature.
+ */
+ V3_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be
+ * parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v3 signer's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})
+ * - Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})
+ * - Parameter 3: error details ({@code Throwable})
+ *
+ */
+ V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"),
+
+ /**
+ * Failed to parse this signer's digest record contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains a malformed additional attribute.
+ *
+ *
+ * - Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v3 signature contains no signers.
+ */
+ V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"),
+
+ /**
+ * APK Signature Scheme v3 signature contains multiple signers (only one allowed per
+ * platform version).
+ */
+ V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single "
+ + " platform version."),
+
+ /**
+ * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers
+ * found, where only one may be used with APK Signature Scheme v3
+ */
+ V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK "
+ + " Signature Scheme v3 signer. Only one allowed."),
+
+ /**
+ * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers,
+ * or have them as the root of its signing certificate history
+ */
+ V3_SIG_PAST_SIGNERS_MISMATCH(
+ "v3 signer differs from v1/v2 signer without proper signing certificate lineage."),
+
+ /**
+ * This APK Signature Scheme v3 signer contains a signature produced using an unknown
+ * algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains an unknown additional attribute.
+ *
+ *
+ * - Parameter 1: attribute ID ({@code Integer})
+ *
+ */
+ V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK
+ * versions.
+ *
+ *
+ * - Parameter 1: minSdkVersion ({@code Integer})
+ *
- Parameter 2: maxSdkVersion ({@code Integer})
+ *
+ */
+ V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature "
+ + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"),
+
+ /**
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers no signatures.
+ */
+ V3_SIG_NO_SIGNATURES("No signatures"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers signatures but none of them are supported.
+ */
+ V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers no certificates.
+ */
+ V3_SIG_NO_CERTIFICATES("No certificates"),
+
+ /**
+ * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data
+ * does not match the minSdkVersion listed in the signatures record.
+ *
+ *
+ * - Parameter 1: minSdkVersion in signature record ({@code Integer})
+ * - Parameter 2: minSdkVersion in signed data ({@code Integer})
+ *
+ */
+ V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+ "minSdkVersion mismatch between signed data and signature record:"
+ + " <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data
+ * does not match the maxSdkVersion listed in the signatures record.
+ *
+ *
+ * - Parameter 1: maxSdkVersion in signature record ({@code Integer})
+ * - Parameter 2: maxSdkVersion in signed data ({@code Integer})
+ *
+ */
+ V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+ "maxSdkVersion mismatch between signed data and signature record:"
+ + " <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does
+ * not match the public key listed in the signatures record.
+ *
+ *
+ * - Parameter 1: hex-encoded public key from certificate ({@code String})
+ * - Parameter 2: hex-encoded public key from signatures record ({@code String})
+ *
+ */
+ V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures
+ * record do not match the signature algorithms listed in the signatures record.
+ *
+ *
+ * - Parameter 1: signature algorithms from signatures record ({@code List})
+ * - Parameter 2: signature algorithms from digests record ({@code List})
+ *
+ */
+ V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+ "Signature algorithms mismatch between signatures and digests records"
+ + ": %1$s vs %2$s"),
+
+ /**
+ * The APK's digest does not match the digest contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected digest of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual digest of the APK ({@code String})
+ *
+ */
+ V3_SIG_APK_DIGEST_DID_NOT_VERIFY(
+ "APK integrity check failed. %1$s digest mismatch."
+ + " Expected: <%2$s>, actual: <%3$s>"),
+
+ /**
+ * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with
+ * signature(s) that did not verify.
+ */
+ V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation"
+ + " record with signature(s) that did not verify."),
+
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3
+ * signature's additional attributes section.
+ */
+ V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the "
+ + "APK Signature Scheme v3 signature's additional attributes section."),
+
+ /**
+ * The APK's signing certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the signing certificate history
+ */
+ V3_SIG_POR_CERT_MISMATCH(
+ "APK signing certificate differs from the associated certificate found in the "
+ + "signer's SigningCertificateLineage."),
+
+ /**
+ * The APK Signature Scheme v3 signers encountered do not offer a continuous set of
+ * supported platform versions. Either they overlap, resulting in potentially two
+ * acceptable signers for a platform version, or there are holes which would create problems
+ * in the event of platform version upgrades.
+ */
+ V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+ + "versions are not continuous."),
+
+ /**
+ * The APK Signature Scheme v3 signers don't cover all requested SDK versions.
+ *
+ *
+ * - Parameter 1: minSdkVersion ({@code Integer})
+ *
- Parameter 2: maxSdkVersion ({@code Integer})
+ *
+ */
+ V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+ + "versions do not cover the entire desired range. Found min: %1$s max %2$s"),
+
+ /**
+ * The SigningCertificateLineages for different platform versions using APK Signature Scheme
+ * v3 do not go together. Specifically, each should be a subset of another, with the size
+ * of each increasing as the platform level increases.
+ */
+ V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions"
+ + " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
+
+ /**
+ * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+ * was not found.
+ *
+ *
+ * - Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ *
+ */
+ V31_BLOCK_MISSING(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but a v3.1 block was not found"),
+
+ /**
+ * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+ * targeting rotation in the v3.1 signer block.
+ *
+ *
+ * - Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ *
- Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ *
+ */
+ V31_ROTATION_MIN_SDK_MISMATCH(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+ /**
+ * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+ * SDK version stripping protection attribute was not written to the v3 signer.
+ *
+ *
+ * - Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ *
+ */
+ V31_ROTATION_MIN_SDK_ATTR_MISSING(
+ "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+ + " not contain the attribute to detect if this signature is stripped"),
+
+ /**
+ * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+ * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+ * same as the SDK version for rotation then this should be written to a v3.0 block.
+ */
+ V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+ "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+ /**
+ * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in
+ * the signer; this attribute is only intended for v3.1 signers to indicate they should be
+ * targeting the next development release that is using the SDK version of the previously
+ * released platform SDK version.
+ */
+ V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER(
+ "The rotation-targets-dev-release attribute is only supported on v3.1 signers; "
+ + "this attribute will be ignored by the platform in a v3.0 signer"),
+
+ /**
+ * APK Signing Block contains an unknown entry.
+ *
+ *
+ * - Parameter 1: entry ID ({@code Integer})
+ *
+ */
+ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme
+ * V4 signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V4_SIG_MALFORMED_SIGNERS(
+ "V4 signature has malformed signer block"),
+
+ /**
+ * This APK Signature Scheme V4 signer contains a signature produced using an
+ * unknown algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ V4_SIG_UNKNOWN_SIG_ALGORITHM(
+ "V4 signature has unknown signing algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no signatures.
+ */
+ V4_SIG_NO_SIGNATURES(
+ "V4 signature has no signature found"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers signatures but none of them are
+ * supported.
+ */
+ V4_SIG_NO_SUPPORTED_SIGNATURES(
+ "V4 signature has no supported signature"),
+
+ /**
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+ * parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme V4 signer's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})
+ * - Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})
+ * - Parameter 3: error details ({@code Throwable})
+ *
+ */
+ V4_SIG_MALFORMED_CERTIFICATE(
+ "V4 signature has malformed certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no certificate.
+ */
+ V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer's public key listed in the signer's
+ * certificate does not match the public key listed in the signature proto.
+ *
+ *
+ * - Parameter 1: hex-encoded public key from certificate ({@code String})
+ * - Parameter 2: hex-encoded public key from signature proto ({@code String})
+ *
+ */
+ V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK's hash root (aka digest) does not match the hash root contained in the Signature
+ * Scheme V4 signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected digest of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual digest of the APK ({@code String})
+ *
+ */
+ V4_SIG_APK_ROOT_DID_NOT_VERIFY(
+ "V4 signature's hash tree root (content digest) did not verity"),
+
+ /**
+ * The APK's hash tree does not match the hash tree contained in the Signature
+ * Scheme V4 signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected hash tree of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual hash tree of the APK ({@code String})
+ *
+ */
+ V4_SIG_APK_TREE_DID_NOT_VERIFY(
+ "V4 signature's hash tree did not verity"),
+
+ /**
+ * Using more than one Signer to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_MULTIPLE_SIGNERS(
+ "V4 signature only supports one signer"),
+
+ /**
+ * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
+ * used to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_V2_V3_SIGNERS_MISMATCH(
+ "V4 signature and V2/V3 signature have mismatched certificates"),
+
+ V4_SIG_V2_V3_DIGESTS_MISMATCH(
+ "V4 signature and V2/V3 signature have mismatched digests"),
+
+ /**
+ * The v4 signature format version isn't the same as the tool's current version, something
+ * may go wrong.
+ */
+ V4_SIG_VERSION_NOT_CURRENT(
+ "V4 signature format version %1$d is different from the tool's current "
+ + "version %2$d"),
+
+ /**
+ * The APK does not contain the source stamp certificate digest file nor the signature block
+ * when verification expected a source stamp to be present.
+ */
+ SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING(
+ "Neither the source stamp certificate digest file nor the signature block are "
+ + "present in the APK"),
+
+ /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
+ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
+
+ /**
+ * SourceStamp's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"),
+
+ /** Failed to parse SourceStamp's signature. */
+ SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"),
+
+ /**
+ * SourceStamp contains a signature produced using an unknown algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * An exception was encountered while verifying SourceStamp signature.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
- Parameter 2: exception ({@code Throwable})
+ *
+ */
+ SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * SourceStamp signature block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /** SourceStamp offers no signatures. */
+ SOURCE_STAMP_NO_SIGNATURE("No signature"),
+
+ /**
+ * SourceStamp offers an unsupported signature.
+ *
+ * - Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp
+ * signing block.
+ *
- Parameter 2: {@code Exception} caught when attempting to obtain the list of
+ * supported signatures.
+ *
+ */
+ SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"),
+
+ /**
+ * SourceStamp's certificate listed in the APK signing block does not match the certificate
+ * listed in the SourceStamp file in the APK.
+ *
+ *
+ * - Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing
+ * block ({@code String})
+ *
- Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code
+ * String})
+ *
+ */
+ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
+ "Certificate mismatch between SourceStamp block in APK signing block and"
+ + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK contains a source stamp signature block without the expected certificate digest
+ * in the APK contents.
+ */
+ SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST(
+ "A source stamp signature block was found without a corresponding certificate "
+ + "digest in the APK"),
+
+ /**
+ * When verifying just the source stamp, the certificate digest in the APK does not match
+ * the expected digest.
+ *
+ * - Parameter 1: SHA-256 digest of the source stamp certificate in the APK.
+ *
- Parameter 2: SHA-256 digest of the expected source stamp certificate.
+ *
+ */
+ SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH(
+ "The source stamp certificate digest in the APK, %1$s, does not match the "
+ + "expected digest, %2$s"),
+
+ /**
+ * Source stamp block contains a malformed attribute.
+ *
+ *
+ * - Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
+ *
+ */
+ SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"),
+
+ /**
+ * Source stamp block contains an unknown attribute.
+ *
+ *
+ * - Parameter 1: attribute ID ({@code Integer})
+ *
+ */
+ SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"),
+
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage "
+ + "structure in the source stamp attributes section."),
+
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ SOURCE_STAMP_POR_CERT_MISMATCH(
+ "APK signing certificate differs from the associated certificate found in the "
+ + "signer's SigningCertificateLineage."),
+
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute "
+ + "contains a proof-of-rotation record with signature(s) that did not verify."),
+
+ /**
+ * The source stamp timestamp attribute has an invalid value (<= 0).
+ *
+ * - Parameter 1: The invalid timestamp value.
+ *
+ */
+ SOURCE_STAMP_INVALID_TIMESTAMP(
+ "The source stamp"
+ + " timestamp attribute has an invalid value: %1$d"),
+
+ /**
+ * The APK could not be properly parsed due to a ZIP or APK format exception.
+ *
+ * - Parameter 1: The {@code Exception} caught when attempting to parse the APK.
+ *
+ */
+ MALFORMED_APK(
+ "Malformed APK; the following exception was caught when attempting to parse the "
+ + "APK: %1$s"),
+
+ /**
+ * An unexpected exception was caught when attempting to verify the signature(s) within the
+ * APK.
+ *
+ * - Parameter 1: The {@code Exception} caught during verification.
+ *
+ */
+ UNEXPECTED_EXCEPTION(
+ "An unexpected exception was caught when verifying the signature: %1$s");
+
+ private final String mFormat;
+
+ Issue(String format) {
+ mFormat = format;
+ }
+
+ /**
+ * Returns the format string suitable for combining the parameters of this issue into a
+ * readable string. See {@link java.util.Formatter} for format.
+ */
+ private String getFormat() {
+ return mFormat;
+ }
+ }
+
+ /**
+ * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
+ * form.
+ */
+ public static class IssueWithParams extends ApkVerificationIssue {
+ private final Issue mIssue;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code IssueWithParams} of the specified type and with provided
+ * parameters.
+ */
+ public IssueWithParams(Issue issue, Object[] params) {
+ super(issue.mFormat, params);
+ mIssue = issue;
+ mParams = params;
+ }
+
+ /**
+ * Returns the type of this issue.
+ */
+ public Issue getIssue() {
+ return mIssue;
+ }
+
+ /**
+ * Returns the parameters of this issue.
+ */
+ public Object[] getParams() {
+ return mParams.clone();
+ }
+
+ /**
+ * Returns a readable form of this issue.
+ */
+ @Override
+ public String toString() {
+ return String.format(mIssue.getFormat(), mParams);
+ }
+ }
+
+ /**
+ * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate
+ * on the contents of the arrays rather than on references.
+ */
+ private static class ByteArray {
+ private final byte[] mArray;
+ private final int mHashCode;
+
+ private ByteArray(byte[] arr) {
+ mArray = arr;
+ mHashCode = Arrays.hashCode(mArray);
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ByteArray)) {
+ return false;
+ }
+ ByteArray other = (ByteArray) obj;
+ if (hashCode() != other.hashCode()) {
+ return false;
+ }
+ if (!Arrays.equals(mArray, other.mArray)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Builder of {@link ApkVerifier} instances.
+ *
+ * The resulting verifier by default checks whether the APK will verify on all platform
+ * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in
+ * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using
+ * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}.
+ */
+ public static class Builder {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+ private File mV4SignatureFile;
+
+ private Integer mMinSdkVersion;
+ private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK file.
+ */
+ public Builder(File apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkFile = apk;
+ mApkDataSource = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK.
+ */
+ public Builder(DataSource apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkDataSource = apk;
+ mApkFile = null;
+ }
+
+ /**
+ * Sets the oldest Android platform version for which the APK is verified. APK verification
+ * will confirm that the APK is expected to install successfully on all known Android
+ * platforms starting from the platform version with the provided API Level. The upper end
+ * of the platform versions range can be modified via
+ * {@link #setMaxCheckedPlatformVersion(int)}.
+ *
+ *
This method is useful for overriding the default behavior which checks that the APK
+ * will verify on all platform versions supported by the APK, as specified by
+ * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets the newest Android platform version for which the APK is verified. APK verification
+ * will confirm that the APK is expected to install successfully on all platform versions
+ * supported by the APK up until and including the provided version. The lower end
+ * of the platform versions range can be modified via
+ * {@link #setMinCheckedPlatformVersion(int)}.
+ *
+ * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+ mMaxSdkVersion = maxSdkVersion;
+ return this;
+ }
+
+ public Builder setV4SignatureFile(File v4SignatureFile) {
+ mV4SignatureFile = v4SignatureFile;
+ return this;
+ }
+
+ /**
+ * Returns an {@link ApkVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public ApkVerifier build() {
+ return new ApkVerifier(
+ mApkFile,
+ mApkDataSource,
+ mV4SignatureFile,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
+
+ /**
+ * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link
+ * IssueWithParams} equivalent.
+ */
+ public static class ApkVerificationIssueAdapter {
+ private ApkVerificationIssueAdapter() {
+ }
+
+ // This field is visible for testing
+ static final Map sVerificationIssueIdToIssue = new HashMap<>();
+
+ static {
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
+ Issue.V2_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS,
+ Issue.V2_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER,
+ Issue.V2_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE,
+ Issue.V2_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES,
+ Issue.V2_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE,
+ Issue.V2_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES,
+ Issue.V2_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST,
+ Issue.V2_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS,
+ Issue.V3_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS,
+ Issue.V3_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER,
+ Issue.V3_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE,
+ Issue.V3_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES,
+ Issue.V3_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE,
+ Issue.V3_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES,
+ Issue.V3_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST,
+ Issue.V3_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE,
+ Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+ Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE,
+ Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION,
+ Issue.SOURCE_STAMP_VERIFY_EXCEPTION);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK,
+ Issue.MALFORMED_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION,
+ Issue.UNEXPECTED_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+ Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE,
+ Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE,
+ Issue.SOURCE_STAMP_MALFORMED_LINEAGE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH,
+ Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+ Issue.JAR_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ Issue.JAR_SIG_PARSE_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ /**
+ * Converts the provided {@code verificationIssues} to a {@code List} of corresponding
+ * {@link IssueWithParams} instances.
+ */
+ public static List getIssuesFromVerificationIssues(
+ List extends ApkVerificationIssue> verificationIssues) {
+ List result = new ArrayList<>(verificationIssues.size());
+ for (ApkVerificationIssue issue : verificationIssues) {
+ if (issue instanceof IssueWithParams) {
+ result.add((IssueWithParams) issue);
+ } else {
+ result.add(
+ new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()),
+ issue.getParams()));
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/Constants.java b/app/src/main/java/com/android/apksig/Constants.java
new file mode 100644
index 00000000..f64064c7
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/Constants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 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 com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+
+/**
+ * Exports internally defined constants to allow clients to reference these values without relying
+ * on internal code.
+ */
+public class Constants {
+ private Constants() {}
+
+ public static final int VERSION_SOURCE_STAMP = 0;
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+ public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+ public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
+}
diff --git a/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
new file mode 100644
index 00000000..0623556d
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -0,0 +1,1987 @@
+/*
+ * 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 static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+import android.text.TextUtils;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeSigner;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4Signature;
+import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.TeeDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link ApkSignerEngine}.
+ *
+ * Use {@link Builder} to obtain instances of this engine.
+ */
+public class DefaultApkSignerEngine implements ApkSignerEngine {
+
+ // IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
+ // 1. The engine asks its client to output input JAR entries which are not part of JAR
+ // signature.
+ // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
+ // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
+ // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
+ // file. It does not care about individual (i.e., JAR entry-specific) sections. It then
+ // emits the v1 signature (a set of JAR entries) and asks the client to output them.
+ // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
+ // from outputZipSections() and asks its client to insert this block into the output.
+ // 4. If APK Signature Scheme v3 (v3 signing) is enabled, the engine includes it in the APK
+ // Signing BLock output from outputZipSections() and asks its client to insert this block
+ // into the output. If both v2 and v3 signing is enabled, they are both added to the APK
+ // Signing Block before asking the client to insert it into the output.
+
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mV3SigningEnabled;
+ private final boolean mVerityEnabled;
+ private final boolean mDebuggableApkPermitted;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final String mCreatedBy;
+ private final List mSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+ private final int mMinSdkVersion;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private List mPreservedV2Signers = Collections.emptyList();
+ private List> mPreservedSignatureBlocks = Collections.emptyList();
+
+ private List mV1SignerConfigs = Collections.emptyList();
+ private DigestAlgorithm mV1ContentDigestAlgorithm;
+
+ private boolean mClosed;
+
+ private boolean mV1SignaturePending;
+
+ /** Names of JAR entries which this engine is expected to output as part of v1 signing. */
+ private Set mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
+
+ /** Requests for digests of output JAR entries. */
+ private final Map mOutputJarEntryDigestRequests =
+ new HashMap<>();
+
+ /** Digests of output JAR entries. */
+ private final Map mOutputJarEntryDigests = new HashMap<>();
+
+ /** Data of JAR entries emitted by this engine as v1 signature. */
+ private final Map mEmittedSignatureJarEntryData = new HashMap<>();
+
+ /** Requests for data of output JAR entries which comprise the v1 signature. */
+ private final Map mOutputSignatureJarEntryDataRequests =
+ new HashMap<>();
+ /**
+ * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
+ */
+ private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
+
+ /**
+ * Request to obtain the data of AndroidManifest.xml or {@code null} if the request hasn't been
+ * issued.
+ */
+ private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest;
+
+ /**
+ * Whether the package being signed is marked as {@code android:debuggable} or {@code null} if
+ * this is not yet known.
+ */
+ private Boolean mDebuggable;
+
+ /**
+ * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
+ */
+ private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
+
+ private boolean mV2SignaturePending;
+ private boolean mV3SignaturePending;
+
+ /**
+ * Request to output the emitted v2 and/or v3 signature(s) {@code null} if the request hasn't
+ * been issued.
+ */
+ private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+
+ /**
+ * A Set of block IDs to be discarded when requesting to preserve the original signatures.
+ */
+ private static final Set DISCARDED_SIGNATURE_BLOCK_IDS;
+ static {
+ DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3);
+ // The verity padding block is recomputed on an
+ // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID);
+ // The source stamp block is not currently preserved; appending a new signature scheme
+ // block will invalidate the previous source stamp.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID);
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private DefaultApkSignerEngine(
+ List signerConfigs,
+ SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ int minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean verityEnabled,
+ boolean debuggableApkPermitted,
+ boolean otherSignersSignaturesPreserved,
+ String createdBy,
+ SigningCertificateLineage signingCertificateLineage)
+ throws InvalidKeyException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mVerityEnabled = verityEnabled;
+ mV1SignaturePending = v1SigningEnabled;
+ mV2SignaturePending = v2SigningEnabled;
+ mV3SignaturePending = v3SigningEnabled;
+ mDebuggableApkPermitted = debuggableApkPermitted;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mCreatedBy = createdBy;
+ mSignerConfigs = signerConfigs;
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ mSigningCertificateLineage = signingCertificateLineage;
+
+ if (v1SigningEnabled) {
+ if (v3SigningEnabled) {
+
+ // v3 signing only supports single signers, of which the oldest (first) will be the
+ // one to use for v1 and v2 signing
+ SignerConfig oldestConfig = signerConfigs.get(0);
+
+ // in the event of signing certificate changes, make sure we have the oldest in the
+ // signing history to sign with v1
+ if (signingCertificateLineage != null) {
+ SigningCertificateLineage subLineage =
+ signingCertificateLineage.getSubLineage(
+ oldestConfig.mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v1 signing enabled but the oldest signer in the"
+ + " SigningCertificateLineage is missing. Please provide the"
+ + " oldest signer to enable v1 signing");
+ }
+ }
+ createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
+ } else {
+ createV1SignerConfigs(signerConfigs, minSdkVersion);
+ }
+ }
+ }
+
+ private void createV1SignerConfigs(List signerConfigs, int minSdkVersion)
+ throws InvalidKeyException {
+ mV1SignerConfigs = new ArrayList<>(signerConfigs.size());
+ Map v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size());
+ DigestAlgorithm v1ContentDigestAlgorithm = null;
+ for (int i = 0; i < signerConfigs.size(); i++) {
+ SignerConfig signerConfig = signerConfigs.get(i);
+ List certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName());
+ // Check whether the signer's name is unique among all v1 signers
+ Integer indexOfOtherSignerWithSameName = v1SignerNameToSignerIndex.put(v1SignerName, i);
+ if (indexOfOtherSignerWithSameName != null) {
+ throw new IllegalArgumentException(
+ "Signers #"
+ + (indexOfOtherSignerWithSameName + 1)
+ + " and #"
+ + (i + 1)
+ + " have the same name: "
+ + v1SignerName
+ + ". v1 signer names must be unique");
+ }
+
+ DigestAlgorithm v1SignatureDigestAlgorithm =
+ V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion);
+ V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
+ v1SignerConfig.name = v1SignerName;
+ v1SignerConfig.privateKey = signerConfig.getPrivateKey();
+ v1SignerConfig.certificates = certificates;
+ v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+ v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning();
+ // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm
+ // of comparable strength to the digest algorithm used for computing the signature.
+ // When there are multiple signers, pick the strongest digest algorithm out of their
+ // signature digest algorithms. This avoids reducing the digest strength used by any
+ // of the signers to protect APK contents.
+ if (v1ContentDigestAlgorithm == null) {
+ v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+ } else {
+ if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
+ v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+ > 0) {
+ v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+ }
+ }
+ mV1SignerConfigs.add(v1SignerConfig);
+ }
+ mV1ContentDigestAlgorithm = v1ContentDigestAlgorithm;
+ mSignatureExpectedOutputJarEntryNames =
+ V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs);
+ }
+
+ private List createV2SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ if (mV3SigningEnabled) {
+
+ // v3 signing only supports single signers, of which the oldest (first) will be the one
+ // to use for v1 and v2 signing
+ List signerConfig = new ArrayList<>();
+
+ SignerConfig oldestConfig = mSignerConfigs.get(0);
+
+ // first make sure that if we have signing certificate history that the oldest signer
+ // corresponds to the oldest ancestor
+ if (mSigningCertificateLineage != null) {
+ SigningCertificateLineage subLineage =
+ mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v2 signing enabled but the oldest signer in"
+ + " the SigningCertificateLineage is missing. Please provide"
+ + " the oldest signer to enable v2 signing.");
+ }
+ }
+ signerConfig.add(
+ createSigningBlockSignerConfig(
+ mSignerConfigs.get(0),
+ apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ return signerConfig;
+ } else {
+ return createSigningBlockSignerConfigs(
+ apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ }
+ }
+
+ private boolean signingLineageHas31Support() {
+ return mSigningCertificateLineage != null
+ && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+ && mMinSdkVersion < mRotationMinSdkVersion;
+ }
+
+ private List processV3Configs(
+ List rawConfigs) throws InvalidKeyException {
+ // While the V3 signature scheme supports rotation, it is possible for a caller to specify
+ // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1;
+ // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block
+ // will use the original signing key.
+ if (signingLineageHas31Support()) {
+ SigningCertificateLineage subLineage = mSigningCertificateLineage
+ .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage"
+ + " for the v3.0 signing block is missing. Please provide"
+ + " the oldest signer to enable v3.1 signing.");
+ }
+ }
+
+ List processedConfigs = new ArrayList<>();
+
+ // we have our configs, now touch them up to appropriately cover all SDK levels since APK
+ // signature scheme v3 was introduced
+ int currentMinSdk = Integer.MAX_VALUE;
+ for (int i = rawConfigs.size() - 1; i >= 0; i--) {
+ ApkSigningBlockUtils.SignerConfig config = rawConfigs.get(i);
+ if (config.signatureAlgorithms == null) {
+ // no valid algorithm was found for this signer, and we haven't yet covered all
+ // platform versions, something's wrong
+ String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm();
+ throw new InvalidKeyException(
+ "Unsupported key algorithm "
+ + keyAlgorithm
+ + " is "
+ + "not supported for APK Signature Scheme v3 signing");
+ }
+ if (i == rawConfigs.size() - 1) {
+ // first go through the loop, config should support all future platform versions.
+ // this assumes we don't deprecate support for signers in the future. If we do,
+ // this needs to change
+ config.maxSdkVersion = Integer.MAX_VALUE;
+ } else {
+ if (mRotationTargetsDevRelease && currentMinSdk == mRotationMinSdkVersion) {
+ // The currentMinSdk is both the SDK version for the active development release
+ // as well as the most recent released platform. To ensure the v3.0 signer will
+ // target the released platform, overlap the maxSdkVersion for the v3.0 signer
+ // with the minSdkVersion of the rotated signer in the v3.1 block
+ config.maxSdkVersion = currentMinSdk;
+ } else {
+ // otherwise, we only want to use this signer up to the minimum platform version
+ // on which a newer one is acceptable
+ config.maxSdkVersion = currentMinSdk - 1;
+ }
+ }
+ config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
+ // Only use a rotated key and signing lineage if the config's max SDK version is greater
+ // than that requested to support rotation.
+ if (mSigningCertificateLineage != null
+ && ((mRotationTargetsDevRelease
+ ? config.maxSdkVersion > mRotationMinSdkVersion
+ : config.maxSdkVersion >= mRotationMinSdkVersion))) {
+ config.mSigningCertificateLineage =
+ mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
+ if (config.minSdkVersion < mRotationMinSdkVersion) {
+ config.minSdkVersion = mRotationMinSdkVersion;
+ }
+ }
+ // we know that this config will be used, so add it to our result, order doesn't matter
+ // at this point (and likely only one will be needed
+ processedConfigs.add(config);
+ currentMinSdk = config.minSdkVersion;
+ // If the rotation is targeting a development release and this is the v3.1 signer, then
+ // the minSdkVersion of this signer should equal the maxSdkVersion of the next signer;
+ // this ensures a package with the minSdkVersion set to the mRotationMinSdkVersion has
+ // a v3.0 block with the min / max SDK version set to this same minSdkVersion from the
+ // v3.1 block.
+ if ((mRotationTargetsDevRelease && currentMinSdk < mMinSdkVersion)
+ || (!mRotationTargetsDevRelease && currentMinSdk <= mMinSdkVersion)
+ || currentMinSdk <= AndroidSdkVersion.P) {
+ // this satisfies all we need, stop here
+ break;
+ }
+ }
+ if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) {
+ // we can't cover all desired SDK versions, abort
+ throw new InvalidKeyException(
+ "Provided key algorithms not supported on all desired "
+ + "Android SDK versions");
+ }
+
+ return processedConfigs;
+ }
+
+ private List createV3SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+ }
+
+ private List processV31SignerConfigs(
+ List v3SignerConfigs) {
+ // If the signing key has been rotated, the caller has requested to use the rotated
+ // signing key starting from an SDK version where v3.1 is supported, and the minimum
+ // SDK version for the APK is less than the requested rotation minimum, then the APK
+ // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0
+ // scheme with the original signing key. If the APK's minSdkVersion is >= the requested
+ // SDK version for rotation then just use the v3.0 signing block for this.
+ if (!signingLineageHas31Support()) {
+ return null;
+ }
+
+ List v31SignerConfigs = new ArrayList<>();
+ Iterator v3SignerIterator =
+ v3SignerConfigs.iterator();
+ while (v3SignerIterator.hasNext()) {
+ ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+ // All signing configs with a min SDK version that supports v3.1 should be used
+ // in the v3.1 signing block and removed from the v3.0 block.
+ if (signerConfig.minSdkVersion >= mRotationMinSdkVersion) {
+ v31SignerConfigs.add(signerConfig);
+ v3SignerIterator.remove();
+ }
+ }
+ return v31SignerConfigs;
+ }
+
+ private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+ List v4Configs = createSigningBlockSignerConfigs(true,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ if (v4Configs.size() != 1) {
+ // V4 uses signer config to connect back to v3. Use the same filtering logic.
+ v4Configs = processV3Configs(v4Configs);
+ }
+ List v41configs = processV31SignerConfigs(v4Configs);
+ return new V4SchemeSigner.SignerConfig(v4Configs, v41configs);
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
+ throws InvalidKeyException {
+ ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
+ mSourceStampSignerConfig,
+ /* apkSigningBlockPaddingSupported= */ false,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ if (mSourceStampSigningCertificateLineage != null) {
+ config.mSigningCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+ config.certificates.get(0));
+ }
+ return config;
+ }
+
+ private int getMinSdkFromV3SignatureAlgorithms(List algorithms) {
+ int min = Integer.MAX_VALUE;
+ for (SignatureAlgorithm algorithm : algorithms) {
+ int current = algorithm.getMinSdkVersion();
+ if (current < min) {
+ if (current <= mMinSdkVersion || current <= AndroidSdkVersion.P) {
+ // this algorithm satisfies all of our needs, no need to keep looking
+ return current;
+ } else {
+ min = current;
+ }
+ }
+ }
+ return min;
+ }
+
+ private List createSigningBlockSignerConfigs(
+ boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
+ List signerConfigs =
+ new ArrayList<>(mSignerConfigs.size());
+ for (int i = 0; i < mSignerConfigs.size(); i++) {
+ SignerConfig signerConfig = mSignerConfigs.get(i);
+ signerConfigs.add(
+ createSigningBlockSignerConfig(
+ signerConfig, apkSigningBlockPaddingSupported, schemeId));
+ }
+ return signerConfigs;
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
+ SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
+ throws InvalidKeyException {
+ List certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
+ newSignerConfig.privateKey = signerConfig.getPrivateKey();
+ newSignerConfig.certificates = certificates;
+
+ switch (schemeId) {
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+ newSignerConfig.signatureAlgorithms =
+ V2SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
+ } catch (InvalidKeyException e) {
+
+ // It is possible for a signer used for v1/v2 signing to not be allowed for use
+ // with v3 signing. This is ok as long as there exists a more recent v3 signer
+ // that covers all supported platform versions. Populate signatureAlgorithm
+ // with null, it will be cleaned-up in a later step.
+ newSignerConfig.signatureAlgorithms = null;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V4SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported,
+ signerConfig.getDeterministicDsaSigning());
+ } catch (InvalidKeyException e) {
+ // V4 is an optional signing schema, ok to proceed without.
+ newSignerConfig.signatureAlgorithms = null;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ newSignerConfig.signatureAlgorithms =
+ Collections.singletonList(
+ SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
+ }
+ return newSignerConfig;
+ }
+
+ private boolean isDebuggable(String entryName) {
+ return mDebuggableApkPermitted
+ || !ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName);
+ }
+
+ /**
+ * Initializes DefaultApkSignerEngine with the existing MANIFEST.MF. This reads existing digests
+ * from the MANIFEST.MF file (they are assumed correct) and stores them for the final signature
+ * without recalculation. This step has a significant performance benefit in case of incremental
+ * build.
+ *
+ * This method extracts and stored computed digest for every entry that it would compute it
+ * for in the {@link #outputJarEntry(String)} method
+ *
+ * @param manifestBytes raw representation of MANIFEST.MF file
+ * @param entryNames a set of expected entries names
+ * @return set of entry names which were processed by the engine during the initialization, a
+ * subset of entryNames
+ */
+ @Override
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ public Set initWith(byte[] manifestBytes, Set entryNames) {
+ V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
+ Pair> sections =
+ V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
+ String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
+ for (Map.Entry entry : sections.getSecond().entrySet()) {
+ String entryName = entry.getKey();
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
+ && isDebuggable(entryName)) {
+
+ V1SchemeVerifier.NamedDigest extractedDigest = null;
+ Collection digestsToVerify =
+ V1SchemeVerifier.getDigestsToVerify(
+ entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE);
+ for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) {
+ if (digestToVerify.jcaDigestAlgorithm.equals(alg)) {
+ extractedDigest = digestToVerify;
+ break;
+ }
+ }
+ if (extractedDigest != null) {
+ mOutputJarEntryDigests.put(entryName, extractedDigest.digest);
+ }
+ }
+ }
+ return mOutputJarEntryDigests.keySet();
+ }
+
+ @Override
+ public void setExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ public void inputApkSigningBlock(DataSource apkSigningBlock) {
+ checkNotClosed();
+
+ if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
+ return;
+ }
+
+ if (mOtherSignersSignaturesPreserved) {
+ boolean schemeSignatureBlockPreserved = false;
+ mPreservedSignatureBlocks = new ArrayList<>();
+ try {
+ List> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
+ for (Pair signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+ // If a V2 signature block is found and the engine is configured to use V2
+ // then save any of the previous signers that are not part of the current
+ // signing request.
+ if (mV2SigningEnabled) {
+ List, byte[]>> v2Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ mPreservedV2Signers = new ArrayList<>(v2Signers.size());
+ for (Pair, byte[]> v2Signer : v2Signers) {
+ if (!isConfiguredWithSigner(v2Signer.getFirst())) {
+ mPreservedV2Signers.add(v2Signer.getSecond());
+ schemeSignatureBlockPreserved = true;
+ }
+ }
+ } else {
+ // else V2 signing is not enabled; save the entire signature block to be
+ // added to the final APK signing block.
+ mPreservedSignatureBlocks.add(signatureBlock);
+ schemeSignatureBlockPreserved = true;
+ }
+ } else if (signatureBlock.getSecond()
+ == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+ // Preserving other signers in the presence of a V3 signature block is only
+ // supported if the engine is configured to resign the APK with the V3
+ // signature scheme, and the V3 signer in the signature block is the same
+ // as the engine is configured to use.
+ if (!mV3SigningEnabled) {
+ throw new IllegalStateException(
+ "Preserving an existing V3 signature is not supported");
+ }
+ List, byte[]>> v3Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ if (v3Signers.size() > 1) {
+ throw new IllegalArgumentException(
+ "The provided APK signing block contains " + v3Signers.size()
+ + " V3 signers; the V3 signature scheme only supports"
+ + " one signer");
+ }
+ // If there is only a single V3 signer then ensure it is the signer
+ // configured to sign the APK.
+ if (v3Signers.size() == 1
+ && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) {
+ throw new IllegalStateException(
+ "The V3 signature scheme only supports one signer; a request "
+ + "was made to preserve the existing V3 signature, "
+ + "but the engine is configured to sign with a "
+ + "different signer");
+ }
+ } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains(
+ signatureBlock.getSecond())) {
+ mPreservedSignatureBlocks.add(signatureBlock);
+ }
+ }
+ } catch (ApkFormatException | CertificateException | IOException e) {
+ throw new IllegalArgumentException("Unable to parse the provided signing block", e);
+ }
+ // Signature scheme V3+ only support a single signer; if the engine is configured to
+ // sign with V3+ then ensure no scheme signature blocks have been preserved.
+ if (mV3SigningEnabled && schemeSignatureBlockPreserved) {
+ throw new IllegalStateException(
+ "Signature scheme V3+ only supports a single signer and cannot be "
+ + "appended to the existing signature scheme blocks");
+ }
+ return;
+ }
+ }
+
+ /**
+ * Returns whether the engine is configured to sign the APK with a signer using the specified
+ * {@code signerCerts}.
+ */
+ private boolean isConfiguredWithSigner(List signerCerts) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ if (signerCerts.containsAll(signerConfig.getCertificates())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public InputJarEntryInstructions inputJarEntry(String entryName) {
+ checkNotClosed();
+
+ InputJarEntryInstructions.OutputPolicy outputPolicy =
+ getInputJarEntryOutputPolicy(entryName);
+ switch (outputPolicy) {
+ case SKIP:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
+ case OUTPUT:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
+ case OUTPUT_BY_ENGINE:
+ if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ // We copy the main section of the JAR manifest from input to output. Thus, this
+ // invalidates v1 signature and we need to see the entry's data.
+ mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
+ mInputJarManifestEntryDataRequest);
+ }
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
+ default:
+ throw new RuntimeException("Unsupported output policy: " + outputPolicy);
+ }
+ }
+
+ @Override
+ public InspectJarEntryRequest outputJarEntry(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+
+ if (!isDebuggable(entryName)) {
+ forgetOutputApkDebuggableStatus();
+ }
+
+ if (!mV1SigningEnabled) {
+ // No need to inspect JAR entries when v1 signing is not enabled.
+ if (!isDebuggable(entryName)) {
+ // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+ // check whether it declares that the APK is debuggable
+ mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return mOutputAndroidManifestEntryDataRequest;
+ }
+ return null;
+ }
+ // v1 signing is enabled
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature. We thus need to inspect the entry's data to
+ // compute its digest(s) for v1 signature.
+
+ // TODO: Handle the case where other signer's v1 signatures are present and need to be
+ // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
+ // covered by v1 signature.
+ invalidateV1Signature();
+ GetJarEntryDataDigestRequest dataDigestRequest =
+ new GetJarEntryDataDigestRequest(
+ entryName,
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
+ mOutputJarEntryDigests.remove(entryName);
+
+ if ((!mDebuggableApkPermitted)
+ && (ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName))) {
+ // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+ // check whether it declares that the APK is debuggable
+ mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return new CompoundInspectJarEntryRequest(
+ entryName, mOutputAndroidManifestEntryDataRequest, dataDigestRequest);
+ }
+
+ return dataDigestRequest;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of v1 signature generated by this engine. We need to check whether
+ // the entry's data is as output by the engine.
+ invalidateV1Signature();
+ GetJarEntryDataRequest dataRequest;
+ if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ dataRequest = new GetJarEntryDataRequest(entryName);
+ mInputJarManifestEntryDataRequest = dataRequest;
+ } else {
+ // If this entry is part of v1 signature which has been emitted by this engine,
+ // check whether the output entry's data matches what the engine emitted.
+ dataRequest =
+ (mEmittedSignatureJarEntryData.containsKey(entryName))
+ ? new GetJarEntryDataRequest(entryName)
+ : null;
+ }
+
+ if (dataRequest != null) {
+ mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
+ }
+ return dataRequest;
+ }
+
+ // This entry is not covered by v1 signature and isn't part of v1 signature.
+ return null;
+ }
+
+ @Override
+ public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ return getInputJarEntryOutputPolicy(entryName);
+ }
+
+ @Override
+ public void outputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+ if (!mV1SigningEnabled) {
+ return;
+ }
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature.
+ invalidateV1Signature();
+ mOutputJarEntryDigests.remove(entryName);
+ mOutputJarEntryDigestRequests.remove(entryName);
+ mOutputSignatureJarEntryDataRequests.remove(entryName);
+ return;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of the v1 signature generated by this engine.
+ invalidateV1Signature();
+ return;
+ }
+ }
+
+ @Override
+ public OutputJarSignatureRequest outputJarEntries()
+ throws ApkFormatException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException {
+ checkNotClosed();
+
+ if (!mV1SignaturePending) {
+ return null;
+ }
+
+ if ((mInputJarManifestEntryDataRequest != null)
+ && (!mInputJarManifestEntryDataRequest.isDone())) {
+ throw new IllegalStateException(
+ "Still waiting to inspect input APK's "
+ + mInputJarManifestEntryDataRequest.getEntryName());
+ }
+
+ for (GetJarEntryDataDigestRequest digestRequest : mOutputJarEntryDigestRequests.values()) {
+ String entryName = digestRequest.getEntryName();
+ if (!digestRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
+ }
+ if (isEligibleForSourceStamp()) {
+ MessageDigest messageDigest =
+ MessageDigest.getInstance(
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ messageDigest.update(generateSourceStampCertificateDigest());
+ mOutputJarEntryDigests.put(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+ }
+ mOutputJarEntryDigestRequests.clear();
+
+ for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
+ if (!dataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + dataRequest.getEntryName());
+ }
+ }
+
+ List apkSigningSchemeIds = new ArrayList<>();
+ if (mV2SigningEnabled) {
+ apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ }
+ if (mV3SigningEnabled) {
+ apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ }
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ if (isEligibleForSourceStamp()) {
+ inputJarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ }
+
+ // Check whether the most recently used signature (if present) is still fine.
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+ List> signatureZipEntries;
+ if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.sign(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ apkSigningSchemeIds,
+ inputJarManifest,
+ mCreatedBy);
+ } catch (CertificateException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ V1SchemeSigner.OutputManifestFile newManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
+ byte[] emittedSignatureManifest =
+ mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME);
+ if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
+ // Emitted v1 signature is no longer valid.
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.signManifest(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ apkSigningSchemeIds,
+ mCreatedBy,
+ newManifest);
+ } catch (CertificateException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ // Emitted v1 signature is still valid. Check whether the signature is there in the
+ // output.
+ signatureZipEntries = new ArrayList<>();
+ for (Map.Entry expectedOutputEntry :
+ mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ // This signature entry hasn't been output.
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ continue;
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ }
+ }
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ return null;
+ }
+ // v1 signature in the output is not valid.
+ }
+ }
+
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ mV1SignaturePending = false;
+ return null;
+ }
+
+ List sigEntries =
+ new ArrayList<>(signatureZipEntries.size());
+ for (Pair entry : signatureZipEntries) {
+ String entryName = entry.getFirst();
+ byte[] entryData = entry.getSecond();
+ sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
+ mEmittedSignatureJarEntryData.put(entryName, entryData);
+ }
+ mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
+ return mAddV1SignatureRequest;
+ }
+
+ @Deprecated
+ @Override
+ public OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+ return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false);
+ }
+
+ @Override
+ public OutputApkSigningBlockRequest2 outputZipSections2(
+ DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+ return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true);
+ }
+
+ private OutputApkSigningBlockRequestImpl outputZipSectionsInternal(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd,
+ boolean apkSigningBlockPaddingSupported)
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) {
+ return null;
+ }
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+
+ // adjust to proper padding
+ Pair paddingPair =
+ ApkSigningBlockUtils.generateApkSigningBlockPadding(
+ zipEntries, apkSigningBlockPaddingSupported);
+ DataSource beforeCentralDir = paddingPair.getFirst();
+ int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+ DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+
+ List> signingSchemeBlocks = new ArrayList<>();
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
+ // If the engine is configured to preserve previous signature blocks and any were found in
+ // the existing APK signing block then add them to the list to be used to generate the
+ // new APK signing block.
+ if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null
+ && !mPreservedSignatureBlocks.isEmpty()) {
+ signingSchemeBlocks.addAll(mPreservedSignatureBlocks);
+ }
+
+ // create APK Signature Scheme V2 Signature if requested
+ if (mV2SigningEnabled) {
+ invalidateV2Signature();
+ List v2SignerConfigs =
+ createV2SignerConfigs(apkSigningBlockPaddingSupported);
+ v2SigningSchemeBlockAndDigests =
+ V2SchemeSigner.generateApkSignatureSchemeV2Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v2SignerConfigs,
+ mV3SigningEnabled,
+ mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
+ signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ if (mV3SigningEnabled) {
+ invalidateV3Signature();
+ List v3SignerConfigs =
+ createV3SignerConfigs(apkSigningBlockPaddingSupported);
+ List v31SignerConfigs = processV31SignerConfigs(
+ v3SignerConfigs);
+ if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) {
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ v31SigningSchemeBlockAndDigests =
+ new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+ v31SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+ zipCentralDirectory, eocd, v3SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (signingLineageHas31Support()) {
+ builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
+ }
+ v3SigningSchemeBlockAndDigests =
+ builder.build().generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ if (isEligibleForSourceStamp()) {
+ ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
+ createSourceStampSignerConfig();
+ Map> signatureSchemeDigestInfos =
+ new HashMap<>();
+ if (mV3SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV2SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV1SigningEnabled) {
+ Map v1SigningSchemeDigests = new HashMap<>();
+ try {
+ // Jar signing related variables must have been already populated at this point
+ // if V1 signing is enabled since it is happening before computations on the APK
+ // signing block (V2/V3/V4/SourceStamp signing).
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ byte[] jarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ // The digest of the jar manifest does not need to be computed in chunks due to
+ // the small size of the manifest.
+ v1SigningSchemeDigests.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+ } catch (ApkFormatException e) {
+ throw new RuntimeException("Failed to generate manifest file", e);
+ }
+ signatureSchemeDigestInfos.put(
+ VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+ }
+ signingSchemeBlocks.add(
+ V2SourceStampSigner.generateSourceStampBlock(
+ sourceStampSignerConfig, signatureSchemeDigestInfos));
+ }
+
+ // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
+ byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+
+ mAddSigningBlockRequest =
+ new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock);
+ return mAddSigningBlockRequest;
+ }
+
+ @Override
+ public void outputDone() {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ checkSigningBlockDoneIfEnabled();
+ }
+
+ @Override
+ public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+ throws SignatureException {
+ if (outputFile == null) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("Missing V4 output file.");
+ }
+ try {
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ /** For external use only to generate V4 & tree separately. */
+ public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+ throws SignatureException {
+ if (sigOutput == null) {
+ throw new SignatureException("Missing V4 output streams.");
+ }
+ try {
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+ Pair pair =
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+ pair.getFirst().writeTo(sigOutput);
+ return pair.getSecond();
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ @Override
+ public boolean isEligibleForSourceStamp() {
+ return mSourceStampSignerConfig != null
+ && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
+ }
+
+ @Override
+ public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+ throw new SignatureException("No certificates configured for stamp");
+ }
+ try {
+ return computeSha256DigestBytes(
+ mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode source stamp certificate", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ mClosed = true;
+
+ mAddV1SignatureRequest = null;
+ mInputJarManifestEntryDataRequest = null;
+ mOutputAndroidManifestEntryDataRequest = null;
+ mDebuggable = null;
+ mOutputJarEntryDigestRequests.clear();
+ mOutputJarEntryDigests.clear();
+ mEmittedSignatureJarEntryData.clear();
+ mOutputSignatureJarEntryDataRequests.clear();
+
+ mAddSigningBlockRequest = null;
+ }
+
+ private void invalidateV1Signature() {
+ if (mV1SigningEnabled) {
+ mV1SignaturePending = true;
+ }
+ invalidateV2Signature();
+ }
+
+ private void invalidateV2Signature() {
+ if (mV2SigningEnabled) {
+ mV2SignaturePending = true;
+ mAddSigningBlockRequest = null;
+ }
+ }
+
+ private void invalidateV3Signature() {
+ if (mV3SigningEnabled) {
+ mV3SignaturePending = true;
+ mAddSigningBlockRequest = null;
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Engine closed");
+ }
+ }
+
+ private void checkV1SigningDoneIfEnabled() {
+ if (!mV1SignaturePending) {
+ return;
+ }
+
+ if (mAddV1SignatureRequest == null) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
+ }
+ if (!mAddV1SignatureRequest.isDone()) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
+ + " been fulfilled");
+ }
+ for (Map.Entry expectedOutputEntry :
+ mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ throw new IllegalStateException(
+ "APK entry "
+ + entryName
+ + " not yet output despite this having been"
+ + " requested");
+ } else if (!actualDataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ throw new IllegalStateException(
+ "Output APK entry " + entryName + " data differs from what was requested");
+ }
+ }
+ mV1SignaturePending = false;
+ }
+
+ private void checkSigningBlockDoneIfEnabled() {
+ if (!mV2SignaturePending && !mV3SignaturePending) {
+ return;
+ }
+ if (mAddSigningBlockRequest == null) {
+ throw new IllegalStateException(
+ "Signed APK Signing BLock not yet generated. Skipped outputZipSections()?");
+ }
+ if (!mAddSigningBlockRequest.isDone()) {
+ throw new IllegalStateException(
+ "APK Signing Block addition of signature(s) requested by"
+ + " outputZipSections() hasn't been fulfilled yet");
+ }
+ mAddSigningBlockRequest = null;
+ mV2SignaturePending = false;
+ mV3SignaturePending = false;
+ }
+
+ private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() throws SignatureException {
+ if (mDebuggableApkPermitted) {
+ return;
+ }
+
+ try {
+ if (isOutputApkDebuggable()) {
+ throw new SignatureException(
+ "APK is debuggable (see android:debuggable attribute) and this engine is"
+ + " configured to refuse to sign debuggable APKs");
+ }
+ } catch (ApkFormatException e) {
+ throw new SignatureException("Failed to determine whether the APK is debuggable", e);
+ }
+ }
+
+ /**
+ * Returns whether the output APK is debuggable according to its {@code android:debuggable}
+ * declaration.
+ */
+ private boolean isOutputApkDebuggable() throws ApkFormatException {
+ if (mDebuggable != null) {
+ return mDebuggable;
+ }
+
+ if (mOutputAndroidManifestEntryDataRequest == null) {
+ throw new IllegalStateException(
+ "Cannot determine debuggable status of output APK because "
+ + ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME
+ + " entry contents have not yet been requested");
+ }
+
+ if (!mOutputAndroidManifestEntryDataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's "
+ + mOutputAndroidManifestEntryDataRequest.getEntryName());
+ }
+ mDebuggable =
+ ApkUtils.getDebuggableFromBinaryAndroidManifest(
+ ByteBuffer.wrap(mOutputAndroidManifestEntryDataRequest.getData()));
+ return mDebuggable;
+ }
+
+ private void forgetOutputApkDebuggableStatus() {
+ mDebuggable = null;
+ }
+
+ /** Returns the output policy for the provided input JAR entry. */
+ private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
+ }
+ if ((mOtherSignersSignaturesPreserved)
+ || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT;
+ }
+ return InputJarEntryInstructions.OutputPolicy.SKIP;
+ }
+
+ private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
+ private final List mAdditionalJarEntries;
+ private volatile boolean mDone;
+
+ private OutputJarSignatureRequestImpl(List additionalZipEntries) {
+ mAdditionalJarEntries =
+ Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
+ }
+
+ @Override
+ public List getAdditionalJarEntries() {
+ return mAdditionalJarEntries;
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static class OutputApkSigningBlockRequestImpl
+ implements OutputApkSigningBlockRequest, OutputApkSigningBlockRequest2 {
+ private final byte[] mApkSigningBlock;
+ private final int mPaddingBeforeApkSigningBlock;
+ private volatile boolean mDone;
+
+ private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock, int paddingBefore) {
+ mApkSigningBlock = apkSigingBlock.clone();
+ mPaddingBeforeApkSigningBlock = paddingBefore;
+ }
+
+ @Override
+ public byte[] getApkSigningBlock() {
+ return mApkSigningBlock.clone();
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+
+ @Override
+ public int getPaddingSizeBeforeApkSigningBlock() {
+ return mPaddingBeforeApkSigningBlock;
+ }
+ }
+
+ /** JAR entry inspection request which obtain the entry's uncompressed data. */
+ private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final Object mLock = new Object();
+
+ private boolean mDone;
+ private DataSink mDataSink;
+ private ByteArrayOutputStream mDataSinkBuf;
+
+ private GetJarEntryDataRequest(String entryName) {
+ mEntryName = entryName;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ if (mDataSinkBuf == null) {
+ mDataSinkBuf = new ByteArrayOutputStream();
+ }
+ if (mDataSink == null) {
+ mDataSink = DataSinks.asDataSink(mDataSinkBuf);
+ }
+ return mDataSink;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getData() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0];
+ }
+ }
+ }
+
+ /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */
+ private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final String mJcaDigestAlgorithm;
+ private final Object mLock = new Object();
+
+ private boolean mDone;
+ private DataSink mDataSink;
+ private MessageDigest mMessageDigest;
+ private byte[] mDigest;
+
+ private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) {
+ mEntryName = entryName;
+ mJcaDigestAlgorithm = jcaDigestAlgorithm;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ if (mDataSink == null) {
+ mDataSink = DataSinks.asDataSink(getMessageDigest());
+ }
+ return mDataSink;
+ }
+ }
+
+ private MessageDigest getMessageDigest() {
+ synchronized (mLock) {
+ if (mMessageDigest == null) {
+ try {
+ mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(
+ mJcaDigestAlgorithm + " MessageDigest not available", e);
+ }
+ }
+ return mMessageDigest;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ mDigest = getMessageDigest().digest();
+ mMessageDigest = null;
+ mDataSink = null;
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getDigest() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return mDigest.clone();
+ }
+ }
+ }
+
+ /** JAR entry inspection request which transparently satisfies multiple such requests. */
+ private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final InspectJarEntryRequest[] mRequests;
+ private final Object mLock = new Object();
+
+ private DataSink mSink;
+
+ private CompoundInspectJarEntryRequest(
+ String entryName, InspectJarEntryRequest... requests) {
+ mEntryName = entryName;
+ mRequests = requests;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ if (mSink == null) {
+ DataSink[] sinks = new DataSink[mRequests.length];
+ for (int i = 0; i < sinks.length; i++) {
+ sinks[i] = mRequests[i].getDataSink();
+ }
+ mSink = new TeeDataSink(sinks);
+ }
+ return mSink;
+ }
+ }
+
+ @Override
+ public void done() {
+ for (InspectJarEntryRequest request : mRequests) {
+ request.done();
+ }
+ }
+ }
+
+ /**
+ * Configuration of a signer.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+ private final boolean mDeterministicDsaSigning;
+
+ private SignerConfig(
+ String name, PrivateKey privateKey, List 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 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 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 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 signing algorithm variant (RFC6979) should be used.
+ */
+ public Builder(String name, PrivateKey privateKey, List certificates,
+ boolean deterministicDsaSigning) {
+ if (TextUtils.isEmpty(name)) 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 DefaultApkSignerEngine} instances. */
+ public static class Builder {
+ private List mSignerConfigs;
+ private SignerConfig mStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final int mMinSdkVersion;
+
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mV3SigningEnabled = true;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
+ private boolean mVerityEnabled = false;
+ private boolean mDebuggableApkPermitted = true;
+ private boolean mOtherSignersSignaturesPreserved;
+ private String mCreatedBy = "1.0 (Android)";
+
+ 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}.
+ *
+ * @param signerConfigs information about signers with which the APK will be signed. At
+ * least one signer configuration must be provided.
+ * @param minSdkVersion API Level of the oldest Android platform on which the APK is
+ * supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code
+ * AndroidManifest.xml}. The higher the version, the stronger signing features will be
+ * enabled.
+ */
+ public Builder(List signerConfigs, int minSdkVersion) {
+ 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);
+ mMinSdkVersion = minSdkVersion;
+ }
+
+ /**
+ * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+ * configuration of this builder.
+ */
+ public DefaultApkSignerEngine build() throws InvalidKeyException {
+
+ if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
+ throw new IllegalStateException(
+ "Builder configured to both enable and disable APK "
+ + "Signature Scheme v3 signing");
+ }
+ if (mV3SigningExplicitlyDisabled) {
+ mV3SigningEnabled = false;
+ } else if (mV3SigningExplicitlyEnabled) {
+ mV3SigningEnabled = true;
+ }
+
+ // make sure our signers are appropriately setup
+ if (mSigningCertificateLineage != null) {
+ try {
+ mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
+ if (!mV3SigningEnabled && mSignerConfigs.size() > 1) {
+
+ // this is a strange situation: we've provided a valid rotation history, but
+ // are only signing with v1/v2. blow up, since we don't know for sure with
+ // which signer the user intended to sign
+ throw new IllegalStateException(
+ "Provided multiple signers which are part of the"
+ + " SigningCertificateLineage, but not signing with APK"
+ + " Signature Scheme v3");
+ }
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(
+ "Provided signer configs do not match the "
+ + "provided SigningCertificateLineage",
+ e);
+ }
+ } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
+ throw new IllegalStateException(
+ "Multiple signing certificates provided for use with APK Signature Scheme"
+ + " v3 without an accompanying SigningCertificateLineage");
+ }
+
+ return new DefaultApkSignerEngine(
+ mSignerConfigs,
+ mStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
+ mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mVerityEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mCreatedBy,
+ mSigningCertificateLineage);
+ }
+
+ /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */
+ public Builder setStampSignerConfig(SignerConfig stampSignerConfig) {
+ mStampSignerConfig = stampSignerConfig;
+ 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 be signed using JAR signing (aka v1 signature scheme).
+ *
+ * By default, the APK will be signed using this scheme.
+ */
+ public Builder setV1SigningEnabled(boolean enabled) {
+ mV1SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+ * scheme).
+ *
+ *
By default, the APK will be signed using this scheme.
+ */
+ public Builder setV2SigningEnabled(boolean enabled) {
+ mV2SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
+ * scheme).
+ *
+ *
By default, the APK will be signed using this scheme.
+ */
+ public Builder setV3SigningEnabled(boolean enabled) {
+ mV3SigningEnabled = enabled;
+ if (enabled) {
+ mV3SigningExplicitlyEnabled = true;
+ } else {
+ mV3SigningExplicitlyDisabled = true;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using the verity signature algorithm in the v2 and
+ * v3 signature blocks.
+ *
+ *
By default, the APK will be signed using the verity signature algorithm for the v2 and
+ * v3 signature schemes.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ 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}.
+ *
+ *
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.
+ */
+ public Builder setDebuggableApkPermitted(boolean permitted) {
+ 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.
+ *
+ *
By default, signatures of other signers are omitted from the output APK.
+ */
+ public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+ mOtherSignersSignaturesPreserved = preserved;
+ return this;
+ }
+
+ /** Sets the value of the {@code Created-By} field in JAR signature files. */
+ public Builder setCreatedBy(String createdBy) {
+ if (createdBy == null) {
+ throw new NullPointerException();
+ }
+ mCreatedBy = createdBy;
+ return this;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop.
+ *
+ *
By default, if a signing lineage is specified with {@link
+ * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+ * V3.1 will be used to only apply the rotation on devices running Android T+.
+ *
+ *
Note:Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ *
This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ *
Note:If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/Hints.java b/app/src/main/java/com/android/apksig/Hints.java
new file mode 100644
index 00000000..3b202746
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/Hints.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 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 java.io.IOException;
+import java.io.DataOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class Hints {
+ /**
+ * Name of hint pattern asset file in APK.
+ */
+ public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
+
+ /**
+ * Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
+ */
+ public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
+
+ private static int clampToInt(long value) {
+ return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
+ }
+
+ public static final class ByteRange {
+ final long start;
+ final long end;
+
+ public ByteRange(long start, long end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ public static final class PatternWithRange {
+ final Pattern pattern;
+ final long offset;
+ final long size;
+
+ public PatternWithRange(String pattern) {
+ this.pattern = Pattern.compile(pattern);
+ this.offset= 0;
+ this.size = Long.MAX_VALUE;
+ }
+
+ public PatternWithRange(String pattern, long offset, long size) {
+ this.pattern = Pattern.compile(pattern);
+ this.offset = offset;
+ this.size = size;
+ }
+
+ public Matcher matcher(CharSequence input) {
+ return this.pattern.matcher(input);
+ }
+
+ public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
+ if (rangeIn.end - rangeIn.start < this.offset) {
+ return null;
+ }
+ long rangeOutStart = rangeIn.start + this.offset;
+ long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
+ this.size);
+ return new ByteRange(rangeOutStart,
+ rangeOutStart + rangeOutSize);
+ }
+ }
+
+ /**
+ * Create a blob of bytes that PinnerService understands as a
+ * sequence of byte ranges to pin.
+ */
+ public static byte[] encodeByteRangeList(List pinByteRanges) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
+ DataOutputStream out = new DataOutputStream(bos);
+ try {
+ for (ByteRange pinByteRange : pinByteRanges) {
+ out.writeInt(clampToInt(pinByteRange.start));
+ out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException("impossible", ex);
+ }
+ return bos.toByteArray();
+ }
+
+ public static ArrayList parsePinPatterns(byte[] patternBlob) {
+ ArrayList pinPatterns = new ArrayList<>();
+ try {
+ for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
+ String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
+ String[] fields = line.split(" ");
+ if (fields.length == 1) {
+ pinPatterns.add(new PatternWithRange(fields[0]));
+ } else if (fields.length == 3) {
+ long start = Long.parseLong(fields[1]);
+ long end = Long.parseLong(fields[2]);
+ pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
+ } else {
+ throw new AssertionError("bad pin pattern line " + line);
+ }
+ }
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException("UTF-8 must be supported", ex);
+ }
+ return pinPatterns;
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/SigningCertificateLineage.java b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java
new file mode 100644
index 00000000..28bdcc1a
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -0,0 +1,1106 @@
+/*
+ * Copyright (C) 2018 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.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.RandomAccessFileDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+
+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.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * APK Signer Lineage.
+ *
+ * The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant. Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ *
Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see Application Signing
+ */
+public class SigningCertificateLineage {
+
+ public final static int MAGIC = 0x3eff39d1;
+
+ private final static int FIRST_VERSION = 1;
+
+ private static final int CURRENT_VERSION = FIRST_VERSION;
+
+ /** accept data from already installed pkg with this cert */
+ private static final int PAST_CERT_INSTALLED_DATA = 1;
+
+ /** accept sharedUserId with pkg with this cert */
+ private static final int PAST_CERT_SHARED_USER_ID = 2;
+
+ /** grant SIGNATURE permissions to pkgs with this cert */
+ private static final int PAST_CERT_PERMISSION = 4;
+
+ /**
+ * Enable updates back to this certificate. WARNING: this effectively removes any benefit of
+ * signing certificate changes, since a compromised key could retake control of an app even
+ * after change, and should only be used if there is a problem encountered when trying to ditch
+ * an older cert.
+ */
+ private static final int PAST_CERT_ROLLBACK = 8;
+
+ /**
+ * Preserve authenticator module-based access in AccountManager gated by signing certificate.
+ */
+ private static final int PAST_CERT_AUTH = 16;
+
+ private final int mMinSdkVersion;
+
+ /**
+ * The signing lineage is just a list of nodes, with the first being the original signing
+ * certificate and the most recent being the one with which the APK is to actually be signed.
+ */
+ private final List mSigningLineage;
+
+ private SigningCertificateLineage(int minSdkVersion, List list) {
+ mMinSdkVersion = minSdkVersion;
+ mSigningLineage = list;
+ }
+
+ private static SigningCertificateLineage createSigningLineage(
+ int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities,
+ SignerConfig child, SignerCapabilities childCapabilities)
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ SigningCertificateLineage signingCertificateLineage =
+ new SigningCertificateLineage(minSdkVersion, new ArrayList<>());
+ signingCertificateLineage =
+ signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities);
+ return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
+ }
+
+ public static SigningCertificateLineage readFromBytes(byte[] lineageBytes)
+ throws IOException {
+ return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes)));
+ }
+
+ public static SigningCertificateLineage readFromFile(File file)
+ throws IOException {
+ if (file == null) {
+ throw new NullPointerException("file == null");
+ }
+ RandomAccessFile inputFile = new RandomAccessFile(file, "r");
+ return readFromDataSource(DataSources.asDataSource(inputFile));
+ }
+
+ public static SigningCertificateLineage readFromDataSource(DataSource dataSource)
+ throws IOException {
+ if (dataSource == null) {
+ throw new NullPointerException("dataSource == null");
+ }
+ ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size());
+ inBuff.order(ByteOrder.LITTLE_ENDIAN);
+ return read(inBuff);
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute.
+ *
+ *
+ * this may not give a complete representation of an APK's signing certificate history,
+ * since the APK may have multiple signers corresponding to different platform versions.
+ * Use readFromApkFile
to handle this case.
+ *
+ * @param attrValue
+ */
+ public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue)
+ throws IOException {
+ List parsedLineage =
+ V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap(
+ attrValue).order(ByteOrder.LITTLE_ENDIAN));
+ int minSdkVersion = calculateMinSdkVersion(parsedLineage);
+ return new SigningCertificateLineage(minSdkVersion, parsedLineage);
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
+ * signature block of the provided APK File.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
+ * or if the V3 signature block does not contain a valid lineage.
+ */
+ public static SigningCertificateLineage readFromApkFile(File apkFile)
+ throws IOException, ApkFormatException {
+ try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
+ DataSource apk = DataSources.asDataSource(f, 0, f.length());
+ return readFromApkDataSource(apk);
+ }
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
+ * signature block of the provided APK DataSource.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
+ * or if the V3 signature block does not contain a valid lineage.
+ */
+ public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
+ throws IOException, ApkFormatException {
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(e.getMessage());
+ }
+
+ List signatureInfoList = new ArrayList<>();
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if there's only a V3 signature block.
+ }
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if the provided APK is not signed with the v3 signature scheme
+ }
+ if (signatureInfoList.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The provided APK does not contain a valid V3 signature block.");
+ }
+
+ List lineages = new ArrayList<>(1);
+ for (SignatureInfo signatureInfo : signatureInfoList) {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signers:
+ // * length-prefixed signed data
+ // * minSDK
+ // * maxSDK
+ // * length-prefixed sequence of length-prefixed signatures
+ // * length-prefixed public key
+ ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ while (signers.hasRemaining()) {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ try {
+ SigningCertificateLineage lineage = readFromSignedData(signedData);
+ lineages.add(lineage);
+ } catch (IllegalArgumentException ignored) {
+ // The current signer block does not contain a valid lineage, but it is possible
+ // another block will.
+ }
+ }
+ }
+
+ SigningCertificateLineage result;
+ if (lineages.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The provided APK does not contain a valid lineage.");
+ } else if (lineages.size() > 1) {
+ result = consolidateLineages(lineages);
+ } else {
+ result = lineages.get(0);
+ }
+ return result;
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided
+ * signed data portion of a signer in a V3 signature block.
+ *
+ * @throws IllegalArgumentException if the provided signed data does not contain a valid
+ * lineage.
+ */
+ public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData)
+ throws IOException, ApkFormatException {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ // * uint-32: minSdkVersion
+ // * uint-32: maxSdkVersion
+ // * length-prefixed sequence of length-prefixed additional attributes:
+ // * uint32: ID
+ // * (length - 4) bytes: value
+ // * uint32: Proof-of-rotation ID: 0x3ba06f8c
+ // * length-prefixed proof-of-rotation structure
+ // consume the digests through the maxSdkVersion to reach the lineage in the attributes
+ getLengthPrefixedSlice(signedData);
+ getLengthPrefixedSlice(signedData);
+ signedData.getInt();
+ signedData.getInt();
+ // iterate over the additional attributes adding any lineages to the List
+ ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+ List lineages = new ArrayList<>(1);
+ while (additionalAttributes.hasRemaining()) {
+ ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ SigningCertificateLineage lineage = readFromV3AttributeValue(value);
+ lineages.add(lineage);
+ }
+ }
+ SigningCertificateLineage result;
+ // There should only be a single attribute with the lineage, but if there are multiple then
+ // attempt to consolidate the lineages.
+ if (lineages.isEmpty()) {
+ throw new IllegalArgumentException("The signed data does not contain a valid lineage.");
+ } else if (lineages.size() > 1) {
+ result = consolidateLineages(lineages);
+ } else {
+ result = lineages.get(0);
+ }
+ return result;
+ }
+
+ public byte[] getBytes() {
+ return write().array();
+ }
+
+ public void writeToFile(File file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException("file == null");
+ }
+ RandomAccessFile outputFile = new RandomAccessFile(file, "rw");
+ writeToDataSink(new RandomAccessFileDataSink(outputFile));
+ }
+
+ public void writeToDataSink(DataSink dataSink) throws IOException {
+ if (dataSink == null) {
+ throw new NullPointerException("dataSink == null");
+ }
+ dataSink.consume(write());
+ }
+
+ /**
+ * Add a new signing certificate to the lineage. This effectively creates a signing certificate
+ * rotation event, forcing APKs which include this lineage to be signed by the new signer. The
+ * flags associated with the new signer are set to a default value.
+ *
+ * @param parent current signing certificate of the containing APK
+ * @param child new signing certificate which will sign the APK contents
+ */
+ public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child)
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ if (parent == null || child == null) {
+ throw new NullPointerException("can't add new descendant to lineage with null inputs");
+ }
+ SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build();
+ return spawnDescendant(parent, child, signerCapabilities);
+ }
+
+ /**
+ * Add a new signing certificate to the lineage. This effectively creates a signing certificate
+ * rotation event, forcing APKs which include this lineage to be signed by the new signer.
+ *
+ * @param parent current signing certificate of the containing APK
+ * @param child new signing certificate which will sign the APK contents
+ * @param childCapabilities flags
+ */
+ public SigningCertificateLineage spawnDescendant(
+ SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities)
+ throws CertificateEncodingException, InvalidKeyException,
+ NoSuchAlgorithmException, SignatureException {
+ if (parent == null) {
+ throw new NullPointerException("parent == null");
+ }
+ if (child == null) {
+ throw new NullPointerException("child == null");
+ }
+ if (childCapabilities == null) {
+ throw new NullPointerException("childCapabilities == null");
+ }
+ if (mSigningLineage.isEmpty()) {
+ throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an"
+ + " empty SigningCertificateLineage: no parent node");
+ }
+
+ // make sure that the parent matches our newest generation (leaf node/sink)
+ SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1);
+ if (!Arrays.equals(currentGeneration.signingCert.getEncoded(),
+ parent.getCertificate().getEncoded())) {
+ throw new IllegalArgumentException("SignerConfig Certificate containing private key"
+ + " to sign the new SigningCertificateLineage record does not match the"
+ + " existing most recent record");
+ }
+
+ // create data to be signed, including the algorithm we're going to use
+ SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent);
+ ByteBuffer prefixedSignedData = ByteBuffer.wrap(
+ V3SigningCertificateLineage.encodeSignedData(
+ child.getCertificate(), signatureAlgorithm.getId()));
+ prefixedSignedData.position(4);
+ ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining());
+ signedDataBuffer.put(prefixedSignedData);
+ byte[] signedData = signedDataBuffer.array();
+
+ // create SignerConfig to do the signing
+ List certificates = new ArrayList<>(1);
+ certificates.add(parent.getCertificate());
+ ApkSigningBlockUtils.SignerConfig newSignerConfig =
+ new ApkSigningBlockUtils.SignerConfig();
+ newSignerConfig.privateKey = parent.getPrivateKey();
+ newSignerConfig.certificates = certificates;
+ newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm);
+
+ // sign it
+ List> signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData);
+
+ // finally, add it to our lineage
+ SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst());
+ byte[] signature = signatures.get(0).getSecond();
+ currentGeneration.sigAlgorithm = sigAlgorithm;
+ SigningCertificateNode childNode =
+ new SigningCertificateNode(
+ child.getCertificate(), sigAlgorithm, null,
+ signature, childCapabilities.getFlags());
+ List lineageCopy = new ArrayList<>(mSigningLineage);
+ lineageCopy.add(childNode);
+ return new SigningCertificateLineage(mMinSdkVersion, lineageCopy);
+ }
+
+ /**
+ * The number of signing certificates in the lineage, including the current signer, which means
+ * this value can also be used to V2determine the number of signing certificate rotations by
+ * subtracting 1.
+ */
+ public int size() {
+ return mSigningLineage.size();
+ }
+
+ private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent)
+ throws InvalidKeyException {
+ PublicKey publicKey = parent.getCertificate().getPublicKey();
+
+ // TODO switch to one signature algorithm selection, or add support for multiple algorithms
+ List algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, false /* verityEnabled */,
+ false /* deterministicDsaSigning */);
+ return algorithms.get(0);
+ }
+
+ private SigningCertificateLineage spawnFirstDescendant(
+ SignerConfig parent, SignerCapabilities signerCapabilities) {
+ if (!mSigningLineage.isEmpty()) {
+ throw new IllegalStateException("SigningCertificateLineage already has its first node");
+ }
+
+ // check to make sure that the public key for the first node is acceptable for our minSdk
+ try {
+ getSignatureAlgorithm(parent);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("Algorithm associated with first signing certificate"
+ + " invalid on desired platform versions", e);
+ }
+
+ // create "fake" signed data (there will be no signature over it, since there is no parent
+ SigningCertificateNode firstNode = new SigningCertificateNode(
+ parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags());
+ return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode));
+ }
+
+ private static SigningCertificateLineage read(ByteBuffer inputByteBuffer)
+ throws IOException {
+ ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer);
+ if (inputByteBuffer.remaining() < 8) {
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: insufficient data for header.");
+ }
+
+ if (inputByteBuffer.getInt() != MAGIC) {
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: MAGIC header mismatch.");
+ }
+ return read(inputByteBuffer, inputByteBuffer.getInt());
+ }
+
+ private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version)
+ throws IOException {
+ switch (version) {
+ case FIRST_VERSION:
+ try {
+ List nodes =
+ V3SigningCertificateLineage.readSigningCertificateLineage(
+ getLengthPrefixedSlice(inputByteBuffer));
+ int minSdkVersion = calculateMinSdkVersion(nodes);
+ return new SigningCertificateLineage(minSdkVersion, nodes);
+ } catch (ApkFormatException e) {
+ // unable to get a proper length-prefixed lineage slice
+ throw new RuntimeException("Unable to read list of signing certificate nodes in "
+ + "SigningCertificateLineage", e);
+ }
+ default:
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: unrecognized version.");
+ }
+ }
+
+ private static int calculateMinSdkVersion(List nodes) {
+ if (nodes == null) {
+ throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes");
+ }
+ int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P
+ for (SigningCertificateNode node : nodes) {
+ if (node.sigAlgorithm != null) {
+ int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion();
+ if (nodeMinSdkVersion > minSdkVersion) {
+ minSdkVersion = nodeMinSdkVersion;
+ }
+ }
+ }
+ return minSdkVersion;
+ }
+
+ private ByteBuffer write() {
+ byte[] encodedLineage =
+ V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+ int payloadSize = 4 + 4 + 4 + encodedLineage.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(MAGIC);
+ result.putInt(CURRENT_VERSION);
+ result.putInt(encodedLineage.length);
+ result.put(encodedLineage);
+ result.flip();
+ return result;
+ }
+
+ public byte[] encodeSigningCertificateLineage() {
+ return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+ }
+
+ public List sortSignerConfigs(
+ List signerConfigs) {
+ if (signerConfigs == null) {
+ throw new NullPointerException("signerConfigs == null");
+ }
+
+ // not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers
+ // in most cases) and likely already sorted, so not worth the overhead of doing anything
+ // fancier
+ List sortedSignerConfigs =
+ new ArrayList<>(signerConfigs.size());
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ for (int j = 0; j < signerConfigs.size(); j++) {
+ DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j);
+ if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) {
+ sortedSignerConfigs.add(config);
+ break;
+ }
+ }
+ }
+ if (sortedSignerConfigs.size() != signerConfigs.size()) {
+ throw new IllegalArgumentException("SignerConfigs supplied which are not present in the"
+ + " SigningCertificateLineage");
+ }
+ return sortedSignerConfigs;
+ }
+
+ /**
+ * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+ * config.
+ */
+ public SignerCapabilities getSignerCapabilities(SignerConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ return getSignerCapabilities(cert);
+ }
+
+ /**
+ * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+ * certificate.
+ */
+ public SignerCapabilities getSignerCapabilities(X509Certificate cert) {
+ if (cert == null) {
+ throw new NullPointerException("cert == null");
+ }
+
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ SigningCertificateNode lineageNode = mSigningLineage.get(i);
+ if (lineageNode.signingCert.equals(cert)) {
+ int flags = lineageNode.flags;
+ return new SignerCapabilities.Builder(flags).build();
+ }
+ }
+
+ // the provided signer certificate was not found in the lineage
+ throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+ + ") not found in the SigningCertificateLineage");
+ }
+
+ /**
+ * Updates the SignerCapabilities for the signer in the lineage that matches the provided
+ * config. Only those capabilities that have been modified through the setXX methods will be
+ * updated for the signer to prevent unset default values from being applied.
+ */
+ public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ SigningCertificateNode lineageNode = mSigningLineage.get(i);
+ if (lineageNode.signingCert.equals(cert)) {
+ int flags = lineageNode.flags;
+ SignerCapabilities newCapabilities = new SignerCapabilities.Builder(
+ flags).setCallerConfiguredCapabilities(capabilities).build();
+ lineageNode.flags = newCapabilities.getFlags();
+ return;
+ }
+ }
+
+ // the provided signer config was not found in the lineage
+ throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+ + ") not found in the SigningCertificateLineage");
+ }
+
+ /**
+ * Returns a list containing all of the certificates in the lineage.
+ */
+ public List getCertificatesInLineage() {
+ List certs = new ArrayList<>();
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ X509Certificate cert = mSigningLineage.get(i).signingCert;
+ certs.add(cert);
+ }
+ return certs;
+ }
+
+ /**
+ * Returns {@code true} if the specified config is in the lineage.
+ */
+ public boolean isSignerInLineage(SignerConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ return isCertificateInLineage(cert);
+ }
+
+ /**
+ * Returns {@code true} if the specified certificate is in the lineage.
+ */
+ public boolean isCertificateInLineage(X509Certificate cert) {
+ if (cert == null) {
+ throw new NullPointerException("cert == null");
+ }
+
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ if (mSigningLineage.get(i).signingCert.equals(cert)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int calculateDefaultFlags() {
+ return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
+ | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
+ }
+
+ /**
+ * Returns a new SigingCertificateLineage which terminates at the node corresponding to the
+ * given certificate. This is useful in the event of rotating to a new signing algorithm that
+ * is only supported on some platform versions. It enables a v3 signature to be generated using
+ * this signing certificate and the shortened proof-of-rotation record from this sub lineage in
+ * conjunction with the appropriate SDK version values.
+ *
+ * @param x509Certificate the signing certificate for which to search
+ * @return A new SigningCertificateLineage if the given certificate is present.
+ *
+ * @throws IllegalArgumentException if the provided certificate is not in the lineage.
+ */
+ public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) {
+ if (x509Certificate == null) {
+ throw new NullPointerException("x509Certificate == null");
+ }
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) {
+ return new SigningCertificateLineage(
+ mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1)));
+ }
+ }
+
+ // looks like we didn't find the cert,
+ throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage");
+ }
+
+ /**
+ * Consolidates all of the lineages found in an APK into one lineage, which is the longest one.
+ * In so doing, it also checks that all of the smaller lineages are contained in the largest,
+ * and that they properly cover the desired platform ranges.
+ *
+ * An APK may contain multiple lineages, one for each signer, which correspond to different
+ * supported platform versions. In this event, the lineage(s) from the earlier platform
+ * version(s) need to be present in the most recent (longest) one to make sure that when a
+ * platform version changes.
+ *
+ * This does not verify that the largest lineage corresponds to the most recent supported
+ * platform version. That check requires is performed during v3 verification.
+ */
+ public static SigningCertificateLineage consolidateLineages(
+ List lineages) {
+ if (lineages == null || lineages.isEmpty()) {
+ return null;
+ }
+ int largestIndex = 0;
+ int maxSize = 0;
+
+ // determine the longest chain
+ for (int i = 0; i < lineages.size(); i++) {
+ int curSize = lineages.get(i).size();
+ if (curSize > maxSize) {
+ largestIndex = i;
+ maxSize = curSize;
+ }
+ }
+
+ List largestList = lineages.get(largestIndex).mSigningLineage;
+ // make sure all other lineages fit into this one, with the same capabilities
+ for (int i = 0; i < lineages.size(); i++) {
+ if (i == largestIndex) {
+ continue;
+ }
+ List underTest = lineages.get(i).mSigningLineage;
+ if (!underTest.equals(largestList.subList(0, underTest.size()))) {
+ throw new IllegalArgumentException("Inconsistent SigningCertificateLineages. "
+ + "Not all lineages are subsets of each other.");
+ }
+ }
+
+ // if we've made it this far, they all check out, so just return the largest
+ return lineages.get(largestIndex);
+ }
+
+ /**
+ * Representation of the capabilities the APK would like to grant to its old signing
+ * certificates. The {@code SigningCertificateLineage} provides two conceptual data structures.
+ * 1) proof of rotation - Evidence that other parties can trust an APK's current signing
+ * certificate if they trust an older one in this lineage
+ * 2) self-trust - certain capabilities may have been granted by an APK to other parties based
+ * on its own signing certificate. When it changes its signing certificate it may want to
+ * allow the other parties to retain those capabilities.
+ * {@code SignerCapabilties} provides a representation of the second structure.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerCapabilities {
+ private final int mFlags;
+
+ private final int mCallerConfiguredFlags;
+
+ private SignerCapabilities(int flags) {
+ this(flags, 0);
+ }
+
+ private SignerCapabilities(int flags, int callerConfiguredFlags) {
+ mFlags = flags;
+ mCallerConfiguredFlags = callerConfiguredFlags;
+ }
+
+ private int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns {@code true} if the capabilities of this object match those of the provided
+ * object.
+ */
+ public boolean equals(SignerCapabilities other) {
+ return this.mFlags == other.mFlags;
+ }
+
+ /**
+ * Returns {@code true} if this object has the installed data capability.
+ */
+ public boolean hasInstalledData() {
+ return (mFlags & PAST_CERT_INSTALLED_DATA) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the shared UID capability.
+ */
+ public boolean hasSharedUid() {
+ return (mFlags & PAST_CERT_SHARED_USER_ID) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the permission capability.
+ */
+ public boolean hasPermission() {
+ return (mFlags & PAST_CERT_PERMISSION) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the rollback capability.
+ */
+ public boolean hasRollback() {
+ return (mFlags & PAST_CERT_ROLLBACK) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the auth capability.
+ */
+ public boolean hasAuth() {
+ return (mFlags & PAST_CERT_AUTH) != 0;
+ }
+
+ /**
+ * Builder of {@link SignerCapabilities} instances.
+ */
+ public static class Builder {
+ private int mFlags;
+
+ private int mCallerConfiguredFlags;
+
+ /**
+ * Constructs a new {@code Builder}.
+ */
+ public Builder() {
+ mFlags = calculateDefaultFlags();
+ }
+
+ /**
+ * Constructs a new {@code Builder} with the initial capabilities set to the provided
+ * flags.
+ */
+ public Builder(int flags) {
+ mFlags = flags;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object. This flag
+ * is used by the platform to determine if installed data associated with previous
+ * signing certificate should be trusted. In particular, this capability is required to
+ * perform signing certificate rotation during an upgrade on-device. Without it, the
+ * platform will not permit the app data from the old signing certificate to
+ * propagate to the new version. Typically, this flag should be set to enable signing
+ * certificate rotation, and may be unset later when the app developer is satisfied that
+ * their install base is as migrated as it will be.
+ */
+ public Builder setInstalledData(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA;
+ if (enabled) {
+ mFlags |= PAST_CERT_INSTALLED_DATA;
+ } else {
+ mFlags &= ~PAST_CERT_INSTALLED_DATA;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to be sharedUid with
+ * other apps which are still signed with the associated signing certificate. This is
+ * useful in situations where sharedUserId apps would like to change their signing
+ * certificate, but can't guarantee the order of updates to those apps.
+ */
+ public Builder setSharedUid(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID;
+ if (enabled) {
+ mFlags |= PAST_CERT_SHARED_USER_ID;
+ } else {
+ mFlags &= ~PAST_CERT_SHARED_USER_ID;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to grant SIGNATURE
+ * permissions to apps signed with the associated signing certificate. Without this
+ * capability, an application signed with the older certificate will not be granted the
+ * SIGNATURE permissions defined by this app. In addition, if multiple apps define the
+ * same SIGNATURE permission, the second one the platform sees will not be installable
+ * if this capability is not set and the signing certificates differ.
+ */
+ public Builder setPermission(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_PERMISSION;
+ if (enabled) {
+ mFlags |= PAST_CERT_PERMISSION;
+ } else {
+ mFlags &= ~PAST_CERT_PERMISSION;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to upgrade to a new
+ * version that is signed by one of its past signing certificates.
+ *
+ * WARNING: this effectively removes any benefit of signing certificate changes,
+ * since a compromised key could retake control of an app even after change, and should
+ * only be used if there is a problem encountered when trying to ditch an older cert
+ *
+ */
+ public Builder setRollback(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_ROLLBACK;
+ if (enabled) {
+ mFlags |= PAST_CERT_ROLLBACK;
+ } else {
+ mFlags &= ~PAST_CERT_ROLLBACK;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_AUTH} flag in this capabilities object. This flag
+ * is used by the platform to determine whether or not privileged access based on
+ * authenticator module signing certificates should be granted.
+ */
+ public Builder setAuth(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_AUTH;
+ if (enabled) {
+ mFlags |= PAST_CERT_AUTH;
+ } else {
+ mFlags &= ~PAST_CERT_AUTH;
+ }
+ return this;
+ }
+
+ /**
+ * Applies the capabilities that were explicitly set in the provided capabilities object
+ * to this builder. Any values that were not set will not be applied to this builder
+ * to prevent unintentinoally setting a capability back to a default value.
+ */
+ public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) {
+ // The mCallerConfiguredFlags should have a bit set for each capability that was
+ // set by a caller. If a capability was explicitly set then the corresponding bit
+ // in mCallerConfiguredFlags should be set. This allows the provided capabilities
+ // to take effect for those set by the caller while those that were not set will
+ // be cleared by the bitwise and and the initial value for the builder will remain.
+ mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) |
+ (capabilities.mFlags & capabilities.mCallerConfiguredFlags);
+ return this;
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerCapabilities build() {
+ return new SignerCapabilities(mFlags, mCallerConfiguredFlags);
+ }
+ }
+ }
+
+ /**
+ * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage}
+ *
+ *
Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final PrivateKey mPrivateKey;
+ private final X509Certificate mCertificate;
+
+ private SignerConfig(
+ PrivateKey privateKey,
+ X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ /**
+ * 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 X509Certificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Builder of {@link SignerConfig} instances.
+ */
+ public static class Builder {
+ private final PrivateKey mPrivateKey;
+ private final X509Certificate mCertificate;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param privateKey signing key
+ * @param certificate the X.509 certificate with a subject public key of the
+ * {@code privateKey}.
+ */
+ public Builder(
+ PrivateKey privateKey,
+ X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(
+ mPrivateKey,
+ mCertificate);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link SigningCertificateLineage} instances.
+ */
+ public static class Builder {
+ private final SignerConfig mOriginalSignerConfig;
+ private final SignerConfig mNewSignerConfig;
+ private SignerCapabilities mOriginalCapabilities;
+ private SignerCapabilities mNewCapabilities;
+ private int mMinSdkVersion;
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param originalSignerConfig first signer in this lineage, parent of the next
+ * @param newSignerConfig new signer in the lineage; the new signing key that the APK will
+ * use
+ */
+ public Builder(
+ SignerConfig originalSignerConfig,
+ SignerConfig newSignerConfig) {
+ if (originalSignerConfig == null || newSignerConfig == null) {
+ throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+ + "new SigningCertificateLineage");
+ }
+ mOriginalSignerConfig = originalSignerConfig;
+ mNewSignerConfig = newSignerConfig;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) on which this lineage is expected
+ * to validate. It is possible that newer signers in the lineage may not be recognized on
+ * the given platform, but as long as an older signer is, the lineage can still be used to
+ * sign an APK for the given platform.
+ *
+ * By default, this value is set to the value for the
+ * P release, since this structure was created for that release, and will also be set to
+ * that value if a smaller one is specified.
+ */
+ public Builder setMinSdkVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an
+ * older signing certificate to still be used in some situations on the platform even though
+ * the APK is now being signed by a newer signing certificate.
+ */
+ public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) {
+ if (signerCapabilities == null) {
+ throw new NullPointerException("signerCapabilities == null");
+ }
+ mOriginalCapabilities = signerCapabilities;
+ return this;
+ }
+
+ /**
+ * Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an
+ * older signing certificate to still be used in some situations on the platform even though
+ * the APK is now being signed by a newer signing certificate. By default, the new signer
+ * will have all capabilities, so when first switching to a new signing certificate, these
+ * capabilities have no effect, but they will act as the default level of trust when moving
+ * to a new signing certificate.
+ */
+ public Builder setNewCapabilities(SignerCapabilities signerCapabilities) {
+ if (signerCapabilities == null) {
+ throw new NullPointerException("signerCapabilities == null");
+ }
+ mNewCapabilities = signerCapabilities;
+ return this;
+ }
+
+ public SigningCertificateLineage build()
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ if (mMinSdkVersion < AndroidSdkVersion.P) {
+ mMinSdkVersion = AndroidSdkVersion.P;
+ }
+
+ if (mOriginalCapabilities == null) {
+ mOriginalCapabilities = new SignerCapabilities.Builder().build();
+ }
+
+ if (mNewCapabilities == null) {
+ mNewCapabilities = new SignerCapabilities.Builder().build();
+ }
+
+ return createSigningLineage(
+ mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities,
+ mNewSignerConfig, mNewCapabilities);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/SourceStampVerifier.java b/app/src/main/java/com/android/apksig/SourceStampVerifier.java
new file mode 100644
index 00000000..b155341b
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,893 @@
+/*
+ * Copyright (C) 2020 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.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK source stamp verifier intended only to verify the validity of the stamp signature.
+ *
+ *
Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
+ * when obtaining the digests for verification. This verifier should only be used in cases where
+ * another mechanism has already been used to verify the APK signatures.
+ */
+public class SourceStampVerifier {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private SourceStampVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ int minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ *
The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
+ * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
+ * can be obtained as follows:
+ *
+ * - Obtain the generic errors via {@link Result#getErrors()}
+ *
- Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ *
- Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ *
- Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
+ * for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
+ *
+ */
+ public SourceStampVerifier.Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ * A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ Result result = new Result();
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ return result;
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+ * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+ * returns the result of the verification.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
+ String expectedCertDigest) {
+ Result result = new Result();
+ try {
+ ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List cdRecords =
+ ZipUtils.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ stampSigningBlockFound = true;
+ } catch (SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ result.addVerificationError(stampSigningBlockFound
+ ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
+ : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ return result;
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
+ sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ result.addVerificationError(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ actualCertDigest, expectedCertDigest);
+ return result;
+ }
+ }
+
+ Map> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
+ }
+ }
+
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
+ signatureSchemeApkContentDigests.isEmpty())) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
+ }
+ }
+
+ if (mMinSdkVersion < AndroidSdkVersion.N
+ || signatureSchemeApkContentDigests.isEmpty()) {
+ Map apkContentDigests =
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ apkContentDigests);
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+ }
+ return result;
+ }
+
+ /**
+ * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
+ * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
+ *
+ * This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ public static void parseSigners(
+ ByteBuffer apkSignatureSchemeBlock,
+ int apkSigSchemeVersion,
+ Map apkContentDigests,
+ Result result) {
+ boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+ // Both the V2 and V3 signature blocks contain the following:
+ // * length-prefixed sequence of length-prefixed signers
+ ByteBuffer signers;
+ try {
+ signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
+ } catch (ApkFormatException e) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+ return;
+ }
+ if (!signers.hasRemaining()) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+ : ApkVerificationIssue.V3_SIG_NO_SIGNERS);
+ return;
+ }
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ while (signers.hasRemaining()) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ if (isV2Block) {
+ result.addV2Signer(signerInfo);
+ } else {
+ result.addV3Signer(signerInfo);
+ }
+ try {
+ ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
+ parseSigner(
+ signer,
+ apkSigSchemeVersion,
+ certFactory,
+ apkContentDigests,
+ signerInfo);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addVerificationWarning(
+ isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses the provided signer block and populates the {@code result}.
+ *
+ * This verifies signatures over {@code signed-data} contained in this block but does not
+ * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+ * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+ * integrity of the APK.
+ *
+ *
This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ private static void parseSigner(
+ ByteBuffer signerBlock,
+ int apkSigSchemeVersion,
+ CertificateFactory certFactory,
+ Map