/*
* 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 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}.
*
* <p>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<SignerConfig> mSignerConfigs;
private final List<SignerConfig> mTargetedSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
private final boolean mSourceStampTimestampEnabled;
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
private List<byte[]> mPreservedV2Signers = Collections.emptyList();
private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList();
private List<V1SchemeSigner.SignerConfig> 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<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
/** Requests for digests of output JAR entries. */
private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
new HashMap<>();
/** Digests of output JAR entries. */
private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
/** Data of JAR entries emitted by this engine as v1 signature. */
private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
/** Requests for data of output JAR entries which comprise the v1 signature. */
private final Map<String, GetJarEntryDataRequest> 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<Integer> 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<SignerConfig> signerConfigs,
List<SignerConfig> targetedSignerConfigs,
SignerConfig sourceStampSignerConfig,
SigningCertificateLineage sourceStampSigningCertificateLineage,
boolean sourceStampTimestampEnabled,
int minSdkVersion,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
boolean verityEnabled,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
String createdBy,
SigningCertificateLineage signingCertificateLineage)
throws InvalidKeyException {
if (signerConfigs.isEmpty() && targetedSignerConfigs.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;
mTargetedSignerConfigs = targetedSignerConfigs;
mSourceStampSignerConfig = sourceStampSignerConfig;
mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
mMinSdkVersion = minSdkVersion;
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.isEmpty() ? signerConfigs.get(0)
: targetedSignerConfigs.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<SignerConfig> signerConfigs, int minSdkVersion)
throws InvalidKeyException {
mV1SignerConfigs = new ArrayList<>(signerConfigs.size());
Map<String, Integer> v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size());
DigestAlgorithm v1ContentDigestAlgorithm = null;
for (int i = 0; i < signerConfigs.size(); i++) {
SignerConfig signerConfig = signerConfigs.get(i);
List<X509Certificate> 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<ApkSigningBlockUtils.SignerConfig> 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<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0)
: mTargetedSignerConfigs.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(
oldestConfig,
apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
return signerConfig;
} else {
return createSigningBlockSignerConfigs(
apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
}
}
private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
// If the caller only specified targeted signing configs, ensure those configs cover the
// full range for V3 support (or the APK's minSdkVersion if > P).
int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion);
if (mSignerConfigs.isEmpty() &&
mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) {
throw new IllegalArgumentException(
"The provided targeted signer configs do not cover the SDK range for V3 "
+ "support; either provide the original signer or ensure a signer "
+ "targets SDK version " + minRequiredV3SdkVersion);
}
List<ApkSigningBlockUtils.SignerConfig> 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 the previous signer was targeting a development release, then the current
// signer's maxSdkVersion should overlap with the previous signer's minSdkVersion
// to ensure the current signer applies to the production release.
ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get(
processedConfigs.size() - 1);
if (prevSigner.signerTargetsDevRelease) {
config.maxSdkVersion = prevSigner.minSdkVersion;
} else {
config.maxSdkVersion = currentMinSdk - 1;
}
}
if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
// If the current signer is targeting the current development release, then set
// the signer's minSdkVersion to the last production release and the flag indicating
// this signer is targeting a dev release.
config.minSdkVersion = V3SchemeConstants.PROD_RELEASE;
config.signerTargetsDevRelease = true;
} else if (config.minSdkVersion == 0) {
config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(
config.signatureAlgorithms);
}
// Truncate the lineage to the current signer if it is not the latest signer.
X509Certificate signerCert = config.certificates.get(0);
if (config.signingCertificateLineage != null
&& !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) {
config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage(
signerCert);
}
// we know that this config will be used, so add it to our result, order doesn't matter
// at this point
processedConfigs.add(config);
currentMinSdk = config.minSdkVersion;
if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion
: currentMinSdk <= minRequiredV3SdkVersion) {
// 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<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
}
private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
// The V3.1 signature scheme supports SDK targeted signing config, but this scheme should
// only be used when a separate signing config exists for the V3.0 block.
if (v3SignerConfigs.size() == 1) {
return null;
}
// When there are multiple signing configs, the signer with the minimum SDK version should
// be used for the V3.0 block, and all other signers should be used for the V3.1 block.
int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt(
signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P);
List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = v3SignerConfigs.iterator();
while (v3SignerIterator.hasNext()) {
ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
// If the signer config's minSdkVersion supports V3.1 and is not the min signer in the
// list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If
// the signer is targeting the minSdkVersion as a development release, then it should
// be included in V3.1 to allow the V3.0 block to target the production release of the
// same SDK version.
if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
&& (signerConfig.minSdkVersion > signerMinSdkVersion
|| (signerConfig.minSdkVersion >= signerMinSdkVersion
&& signerConfig.signerTargetsDevRelease))) {
v31SignerConfigs.add(signerConfig);
v3SignerIterator.remove();
}
}
return v31SignerConfigs;
}
private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
List<ApkSigningBlockUtils.SignerConfig> 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<ApkSigningBlockUtils.SignerConfig> 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.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
config.certificates.get(0));
}
return config;
}
private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> 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<ApkSigningBlockUtils.SignerConfig> createSigningBlockSignerConfigs(
boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
List<ApkSigningBlockUtils.SignerConfig> signerConfigs =
new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size());
for (int i = 0; i < mSignerConfigs.size(); i++) {
SignerConfig signerConfig = mSignerConfigs.get(i);
signerConfigs.add(
createSigningBlockSignerConfig(
signerConfig, apkSigningBlockPaddingSupported, schemeId));
}
if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) {
for (int i = 0; i < mTargetedSignerConfigs.size(); i++) {
SignerConfig signerConfig = mTargetedSignerConfigs.get(i);
signerConfigs.add(
createSigningBlockSignerConfig(
signerConfig, apkSigningBlockPaddingSupported, schemeId));
}
}
return signerConfigs;
}
private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
throws InvalidKeyException {
List<X509Certificate> certificates = signerConfig.getCertificates();
PublicKey publicKey = certificates.get(0).getPublicKey();
ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
newSignerConfig.privateKey = signerConfig.getPrivateKey();
newSignerConfig.certificates = certificates;
newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion();
newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease();
newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage();
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.
*
* <p>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<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
String entryName = entry.getKey();
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
&& isDebuggable(entryName)) {
V1SchemeVerifier.NamedDigest extractedDigest = null;
Collection<V1SchemeVerifier.NamedDigest> 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<Pair<byte[], Integer>> signatureBlocks =
ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
for (Pair<byte[], Integer> 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<Pair<List<X509Certificate>, byte[]>> v2Signers =
ApkSigningBlockUtils.getApkSignatureBlockSigners(
signatureBlock.getFirst());
mPreservedV2Signers = new ArrayList<>(v2Signers.size());
for (Pair<List<X509Certificate>, 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<Pair<List<X509Certificate>, 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<X509Certificate> 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<Integer> 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<Pair<String, byte[]>> 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<String, byte[]> 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<OutputJarSignatureRequest.JarEntry> sigEntries =
new ArrayList<>(signatureZipEntries.size());
for (Pair<String, byte[]> 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<DataSource, Integer> paddingPair =
ApkSigningBlockUtils.generateApkSigningBlockPadding(
zipEntries, apkSigningBlockPaddingSupported);
DataSource beforeCentralDir = paddingPair.getFirst();
int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
List<Pair<byte[], Integer>> 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<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
createV2SignerConfigs(apkSigningBlockPaddingSupported);
v2SigningSchemeBlockAndDigests =
V2SchemeSigner.generateApkSignatureSchemeV2Block(
mExecutor,
beforeCentralDir,
zipCentralDirectory,
eocd,
v2SignerConfigs,
mV3SigningEnabled,
mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (mV3SigningEnabled) {
invalidateV3Signature();
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
createV3SignerConfigs(apkSigningBlockPaddingSupported);
List<ApkSigningBlockUtils.SignerConfig> 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)
.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 (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) {
// The V3.1 stripping protection writes the minimum SDK version from the targeted
// signers as an additional attribute in the V3.0 signing block.
int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt(
signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT);
builder.setMinSdkVersionForV31(minSdkVersionForV31);
}
v3SigningSchemeBlockAndDigests =
builder.build().generateApkSignatureSchemeV3BlockAndDigests();
signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (isEligibleForSourceStamp()) {
ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
createSourceStampSignerConfig();
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> 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<ContentDigestAlgorithm, byte[]> 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);
}
V2SourceStampSigner v2SourceStampSigner =
new V2SourceStampSigner.Builder(sourceStampSignerConfig,
signatureSchemeDigestInfos)
.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled)
.build();
signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock());
}
// 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<V4Signature, byte[]> 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<String, byte[]> 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<JarEntry> mAdditionalJarEntries;
private volatile boolean mDone;
private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
mAdditionalJarEntries =
Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
}
@Override
public List<JarEntry> 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.
*
* <p>Use {@link Builder} to obtain configuration instances.
*/
public static class SignerConfig {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
private final boolean mDeterministicDsaSigning;
private final int mMinSdkVersion;
private final boolean mSignerTargetsDevRelease;
private final SigningCertificateLineage mSigningCertificateLineage;
private SignerConfig(Builder builder) {
mName = builder.mName;
mPrivateKey = builder.mPrivateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
mMinSdkVersion = builder.mMinSdkVersion;
mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease;
mSigningCertificateLineage = builder.mSigningCertificateLineage;
}
/** Returns the name of this signer. */
public String getName() {
return mName;
}
/** Returns the signing key of this signer. */
public PrivateKey getPrivateKey() {
return mPrivateKey;
}
/**
* Returns the certificate(s) of this signer. The first certificate's public key corresponds
* to this signer's private key.
*/
public List<X509Certificate> getCertificates() {
return mCertificates;
}
/**
* If this signer is a DSA signer, whether or not the signing is done deterministically.
*/
public boolean getDeterministicDsaSigning() {
return mDeterministicDsaSigning;
}
/** Returns the minimum SDK version for which this signer should be used. */
public int getMinSdkVersion() {
return mMinSdkVersion;
}
/** Returns whether this signer targets a development release. */
public boolean getSignerTargetsDevRelease() {
return mSignerTargetsDevRelease;
}
/** Returns the {@link SigningCertificateLineage} for this signer. */
public SigningCertificateLineage getSigningCertificateLineage() {
return mSigningCertificateLineage;
}
/** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
private final boolean mDeterministicDsaSigning;
private int mMinSdkVersion;
private boolean mSignerTargetsDevRelease;
private SigningCertificateLineage mSigningCertificateLineage;
/**
* Constructs a new {@code Builder}.
*
* @param name signer's name. The name is reflected in the name of files comprising the
* JAR signature of the APK.
* @param privateKey signing key
* @param certificates list of one or more X.509 certificates. The subject public key of
* the first certificate must correspond to the {@code privateKey}.
*/
public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
this(name, privateKey, certificates, false);
}
/**
* Constructs a new {@code Builder}.
*
* @param name signer's name. The name is reflected in the name of files comprising the
* JAR signature of the APK.
* @param privateKey signing key
* @param certificates list of one or more X.509 certificates. The subject public key of
* the first certificate must correspond to the {@code privateKey}.
* @param deterministicDsaSigning When signing using DSA, whether or not the
* deterministic signing algorithm variant (RFC6979) should be used.
*/
public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates,
boolean deterministicDsaSigning) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
mDeterministicDsaSigning = deterministicDsaSigning;
}
/** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
public Builder setMinSdkVersion(int minSdkVersion) {
return setLineageForMinSdkVersion(null, minSdkVersion);
}
/**
* Sets the specified {@code minSdkVersion} as the minimum Android platform version
* (API level) for which the provided {@code lineage} (where applicable) should be used
* to produce the APK's signature. This method is useful if callers want to specify a
* particular rotated signer or lineage with restricted capabilities for later
* platform releases.
*
* <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
* signing lineages with capabilities; only an app's original signer(s) can be used for
* the V1 and V2 signature blocks. Because of this, only a value of {@code
* minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
* introduced can be specified.
*
* <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
* scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
* the current {@code SignerConfig} being used in the V3.0 signing block and applied to
* Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
* subsequent {@code SignerConfig} instances). Because of this, only a single {@code
* SignerConfig} can be instantiated with a minimum SDK version <= 32.
*
* @param lineage the {@code SigningCertificateLineage} to target the specified {@code
* minSdkVersion}
* @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
* should be used
* @return this {@code Builder} instance
*
* @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
* certificate provided in the constructor is not in the specified {@code lineage}.
*/
public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
int minSdkVersion) {
if (minSdkVersion < AndroidSdkVersion.P) {
throw new IllegalArgumentException(
"SDK targeted signing config is only supported with the V3 signature "
+ "scheme on Android P (SDK version "
+ AndroidSdkVersion.P + ") and later");
}
if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
minSdkVersion = AndroidSdkVersion.P;
}
mMinSdkVersion = minSdkVersion;
// If a lineage is provided, ensure the signing certificate for this signer is in
// the lineage; in the case of multiple signing certificates, the first is always
// used in the lineage.
if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
throw new IllegalArgumentException(
"The provided lineage does not contain the signing certificate, "
+ mCertificates.get(0).getSubjectDN()
+ ", for this SignerConfig");
}
mSigningCertificateLineage = lineage;
return this;
}
/**
* Sets whether this signer's min SDK version is intended to target a development
* release.
*
* <p>This is primarily required for a signer testing on a platform's development
* release; however, it is recommended that signer's use the latest development SDK
* version instead of explicitly specifying this boolean. This class will properly
* handle an SDK that is currently targeting a development release and will use the
* finalized SDK version on release.
*/
private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) {
if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
throw new IllegalArgumentException(
"Rotation can only target a development release for signers targeting "
+ MIN_SDK_WITH_V31_SUPPORT + " or later");
}
mSignerTargetsDevRelease = signerTargetsDevRelease;
return this;
}
/**
* Returns a new {@code SignerConfig} instance configured based on the configuration of
* this builder.
*/
public SignerConfig build() {
return new SignerConfig(this);
}
}
}
/** Builder of {@link DefaultApkSignerEngine} instances. */
public static class Builder {
private List<SignerConfig> mSignerConfigs;
private List<SignerConfig> mTargetedSignerConfigs;
private SignerConfig mStampSignerConfig;
private SigningCertificateLineage mSourceStampSigningCertificateLineage;
private boolean mSourceStampTimestampEnabled = true;
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<SignerConfig> 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;
}
/**
* Sets the APK signature schemes that should be enabled based on the options provided by
* the caller.
*/
private void setEnabledSignatureSchemes() {
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;
}
}
/**
* Sets the SDK targeted signer configs based on the signing config and rotation options
* provided by the caller.
*
* @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created
* from the provided options
*/
private void setTargetedSignerConfigs() throws InvalidKeyException {
// If the caller specified any SDK targeted signer configs, then the min SDK version
// should be set for those configs, all others should have a default 0 min SDK version.
mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion()
- signerConfig2.getMinSdkVersion()));
// With the signer configs sorted, find the first targeted signer config with a min
// SDK version > 0 to create the separate targeted signer configs.
mTargetedSignerConfigs = new ArrayList<>();
for (int i = 0; i < mSignerConfigs.size(); i++) {
if (mSignerConfigs.get(i).getMinSdkVersion() > 0) {
mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size());
mSignerConfigs = mSignerConfigs.subList(0, i);
break;
}
}
// A lineage provided outside a targeted signing config is intended for the original
// rotation; sort the untargeted signing configs based on this lineage and create a new
// targeted signing config for the initial rotation.
if (mSigningCertificateLineage != null) {
if (!mTargetedSignerConfigs.isEmpty()) {
// Only the initial rotation can use the rotation-min-sdk-version; all
// subsequent targeted rotations must use targeted signing configs.
int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion();
if (mRotationMinSdkVersion >= firstTargetedSdkVersion) {
throw new IllegalStateException(
"The rotation-min-sdk-version, " + mRotationMinSdkVersion
+ ", must be less than the first targeted SDK version, "
+ firstTargetedSdkVersion);
}
}
try {
mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
} catch (IllegalArgumentException e) {
throw new IllegalStateException(
"Provided signer configs do not match the "
+ "provided SigningCertificateLineage",
e);
}
// Get the last signer in the lineage, create a new targeted signer from it,
// and add it as a targeted signer config.
SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1);
SignerConfig.Builder rotatedConfigBuilder = new SignerConfig.Builder(
rotatedSignerConfig.getName(), rotatedSignerConfig.getPrivateKey(),
rotatedSignerConfig.getCertificates(),
rotatedSignerConfig.getDeterministicDsaSigning());
rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage,
mRotationMinSdkVersion);
rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease);
mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build());
}
mSigningCertificateLineage = mergeTargetedSigningConfigLineages();
}
/**
* Merges and returns the lineages from any caller provided SDK targeted {@link
* SignerConfig} instances with an optional {@code lineage} specified as part of the general
* signing config.
*
* <p>If multiple signing configs target the same SDK version, or if any of the lineages
* cannot be merged, then an {@code IllegalStateException} is thrown.
*/
private SigningCertificateLineage mergeTargetedSigningConfigLineages()
throws InvalidKeyException {
SigningCertificateLineage mergedLineage = null;
int prevSdkVersion = 0;
for (SignerConfig signerConfig : mTargetedSignerConfigs) {
int signerMinSdkVersion = signerConfig.getMinSdkVersion();
if (signerMinSdkVersion < AndroidSdkVersion.P) {
throw new IllegalStateException(
"Targeted signing config is not supported prior to SDK version "
+ AndroidSdkVersion.P + "; received value "
+ signerMinSdkVersion);
}
SigningCertificateLineage signerLineage =
signerConfig.getSigningCertificateLineage();
// It is possible for a lineage to be null if the user is using one of the
// signers from the lineage as the only signer to target an SDK version; create
// a single element lineage to verify the signer is part of the merged lineage.
if (signerLineage == null) {
try {
signerLineage = new SigningCertificateLineage.Builder(
new SigningCertificateLineage.SignerConfig.Builder(
signerConfig.mPrivateKey,
signerConfig.mCertificates.get(0))
.build())
.build();
} catch (CertificateEncodingException | NoSuchAlgorithmException
| SignatureException e) {
throw new IllegalStateException(
"Unable to create a SignerConfig for signer from certificate "
+ signerConfig.mCertificates.get(0).getSubjectDN());
}
}
// The V3.0 signature scheme does not support verified targeted SDK signing
// configs; if a signer is targeting any SDK version < T, then it will
// target P with the V3.0 signature scheme.
if (signerMinSdkVersion < AndroidSdkVersion.T) {
signerMinSdkVersion = AndroidSdkVersion.P;
}
// Ensure there are no SignerConfigs targeting the same SDK version.
if (signerMinSdkVersion == prevSdkVersion) {
throw new IllegalStateException(
"Multiple SignerConfigs were found targeting SDK version "
+ signerMinSdkVersion);
}
// If multiple lineages have been provided, then verify each subsequent lineage
// is a valid descendant or ancestor of the previously merged lineages.
if (mergedLineage == null) {
mergedLineage = signerLineage;
} else {
try {
mergedLineage = mergedLineage.mergeLineageWith(signerLineage);
} catch (IllegalArgumentException e) {
throw new IllegalStateException(
"The provided lineage targeting SDK " + signerMinSdkVersion
+ " is not in the signing history of the other targeted "
+ "signing configs", e);
}
}
prevSdkVersion = signerMinSdkVersion;
}
return mergedLineage;
}
/**
* Returns a new {@code DefaultApkSignerEngine} instance configured based on the
* configuration of this builder.
*/
public DefaultApkSignerEngine build() throws InvalidKeyException {
setEnabledSignatureSchemes();
setTargetedSignerConfigs();
// make sure our signers are appropriately setup
if (mSigningCertificateLineage != null) {
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");
}
} 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,
mTargetedSignerConfigs,
mStampSignerConfig,
mSourceStampSigningCertificateLineage,
mSourceStampTimestampEnabled,
mMinSdkVersion,
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 source stamp should contain the timestamp attribute with the time
* at which the source stamp was signed.
*/
public Builder setSourceStampTimestampEnabled(boolean value) {
mSourceStampTimestampEnabled = value;
return this;
}
/**
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
*
* <p>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).
*
* <p>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).
*
* <p>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.
*
* <p>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}.
*
* <p>It is dangerous to sign debuggable APKs with production/release keys because Android
* platform loosens security checks for such APKs. For example, arbitrary unauthorized code
* may be executed in the context of such an app by anybody with ADB shell access.
*/
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.
*
* <p>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.
*
* <p>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+.
*
* <p><em>Note:</em>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.
*
* <p>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.
*
* <p><em>Note:</em>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;
}
}
}