Skip to content

Commit

Permalink
Prepare for release 1.8.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
Iurii Makhno committed Aug 11, 2021
1 parent a7bcb53 commit e6f6a86
Show file tree
Hide file tree
Showing 38 changed files with 1,544 additions and 368 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ https://developer.android.com/studio/command-line/bundletool

## Releases

Latest release: [1.7.1](https://github.com/google/bundletool/releases)
Latest release: [1.8.0](https://github.com/google/bundletool/releases)
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ configurations {

// The repackaging rules are defined in the "shadowJar" task below.
dependencies {
compile "com.android.tools:common:30.0.0-alpha10"
compile "com.android.tools:common:30.1.0-alpha07"
compile "com.android.tools:r8:2.2.64"
compile "com.android.tools.build:apkzlib:4.2.0-alpha13"
compile "com.android.tools.build:apksig:4.2.0-alpha13"
compile "com.android.tools.ddms:ddmlib:30.0.0-alpha10"
compile "com.android:zipflinger:7.0.0-alpha14"
compile "com.android.tools.ddms:ddmlib:30.1.0-alpha07"
compile "com.android:zipflinger:7.1.0-alpha07"

shadow "com.android.tools.build:aapt2-proto:7.0.0-beta04-7396180"
shadow "com.google.auto.value:auto-value-annotations:1.6.2"
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
release_version = 1.7.1
release_version = 1.8.0
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public enum ApkBuildMode {
/**
* INSTANT mode only generates instant APKs, assuming at least one module is instant-enabled.
*/
INSTANT;
INSTANT
;

public final String getLowerCaseName() {
return Ascii.toLowerCase(name());
Expand Down Expand Up @@ -173,6 +174,9 @@ public enum OutputFormat {
private static final Flag<Password> STAMP_KEY_PASSWORD_FLAG = Flag.password("stamp-key-pass");
private static final Flag<String> STAMP_SOURCE_FLAG = Flag.string("stamp-source");

// Key-rotation-related flags.
private static final Flag<Path> LINEAGE_FLAG = Flag.path("lineage");
private static final Flag<Path> OLDEST_SIGNER_FLAG = Flag.path("oldest-signer");

private static final String APK_SET_ARCHIVE_EXTENSION = "apks";

Expand All @@ -192,7 +196,7 @@ public enum OutputFormat {
SystemEnvironmentProvider.DEFAULT_PROVIDER
.getProperty("bundletool.serializer.zipflinger")
.map(Boolean::parseBoolean)
.orElse(false);
.orElse(true);

public abstract Path getBundlePath();

Expand Down Expand Up @@ -894,6 +898,58 @@ public static CommandHelp help() {
"The minimum API version for signing the generated APKs using V3 signature"
+ " scheme.")
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(LINEAGE_FLAG.getName())
.setExampleValue("path/to/existing/lineage")
.setOptional(true)
.setDescription(
"Input SigningCertificateLineage. This file contains a binary representation "
+ "of a SigningCertificateLineage object, which contains the "
+ "proof-of-rotation for different signing certificates. This can be used "
+ "with APK Signature Scheme v3 to rotate the signing certificate for an "
+ "APK. An APK previously signed with a SigningCertificateLineage can also "
+ "be specified; the lineage will then be read from the signed data in the "
+ "APK. If set, the flag '%s' must also be set.",
OLDEST_SIGNER_FLAG)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(OLDEST_SIGNER_FLAG.getName())
.setExampleValue("path/to/keystore.properties")
.setOptional(true)
.setDescription(
"Path to a properties file containing the properties of the oldest keystore "
+ "that has previously been used for signing. This is used to sign the "
+ "generated APKs with signature scheme v1 and v2 for backward "
+ "compatibility when the key has been rotated using signature scheme "
+ "v3. If set, the flag '%s' must also be set.\n"
+ "\n"
+ "The file is required to include values for the following properties:\n"
+ " * ks - Path to the keystore.\n"
+ " * ks-key-alias - Alias of the key to use in the keystore.\n"
+ "\n"
+ "The file may also optionally include values for the following "
+ "properties:\n"
+ " * ks-pass - Password of the keystore. If provided, must be prefixed "
+ "with either 'pass:' (if the password is passed in clear text, e.g. "
+ "'pass:qwerty') or 'file:' (if the password is the first line of a "
+ "file, e.g. 'file:/tmp/myPassword.txt'). If this property is not set, "
+ "the password will be requested on the prompt.\n"
+ " * key-pass - Password of the key in the keystore. If provided, must "
+ "be prefixed with either 'pass:' (if the password is passed in clear "
+ "text, e.g. 'pass:qwerty') or 'file:' (if the password is the first "
+ "line of a file, e.g. 'file:/tmp/myPassword.txt'). If this property is "
+ "not set, the keystore password will be tried. If that fails, the "
+ "password will be requested on the prompt.\n"
+ "\n"
+ "Example keystore.properties file:\n"
+ "ks=/path/to/keystore.jks\n"
+ "ks-key-alias=keyAlias\n"
+ "ks-pass=pass:myPassword\n"
+ "key-pass=file:/path/to/myPassword.txt",
LINEAGE_FLAG)
.build())
.addFlag(
FlagDescription.builder()
.setFlagName(CONNECTED_DEVICE_FLAG.getName())
Expand Down Expand Up @@ -1073,7 +1129,8 @@ private static void populateSigningConfigurationFromFlags(
SigningConfiguration.Builder builder =
SigningConfiguration.builder()
.setSignerConfig(signerConfig)
.setMinimumV3SigningApiVersion(minV3SigningApi);
.setMinimumV3RotationApiVersion(minV3SigningApi);
populateLineageFromFlags(builder, flags);
buildApksCommand.setSigningConfiguration(builder.build());
} else if (keystorePath.isPresent() && !keyAlias.isPresent()) {
throw InvalidCommandException.builder()
Expand All @@ -1100,6 +1157,59 @@ private static void populateSigningConfigurationFromFlags(
}
}

private static void populateLineageFromFlags(
SigningConfiguration.Builder signingConfiguration, ParsedFlags flags) {
// Key-rotation-related arguments.
Optional<Path> lineagePath = LINEAGE_FLAG.getValue(flags);
Optional<Path> oldestSignerPropertiesPath = OLDEST_SIGNER_FLAG.getValue(flags);

if (lineagePath.isPresent() && oldestSignerPropertiesPath.isPresent()) {
signingConfiguration.setSigningCertificateLineage(
getLineageFromInputFile(lineagePath.get().toFile()));
KeystoreProperties oldestSignerProperties =
KeystoreProperties.readFromFile(oldestSignerPropertiesPath.get());
signingConfiguration.setOldestSigner(
SignerConfig.extractFromKeystore(
oldestSignerProperties.getKeystorePath(),
oldestSignerProperties.getKeyAlias(),
oldestSignerProperties.getKeystorePassword(),
oldestSignerProperties.getKeyPassword()));
} else if (lineagePath.isPresent() && !oldestSignerPropertiesPath.isPresent()) {
throw InvalidCommandException.builder()
.withInternalMessage(
"Flag '%s' is required when '%s' is set.", OLDEST_SIGNER_FLAG, LINEAGE_FLAG)
.build();
} else if (!lineagePath.isPresent() && oldestSignerPropertiesPath.isPresent()) {
throw InvalidCommandException.builder()
.withInternalMessage(
"Flag '%s' is required when '%s' is set.", LINEAGE_FLAG, OLDEST_SIGNER_FLAG)
.build();
}
}

/** Extracts the Signing Certificate Lineage from the provided lineage or APK file. */
private static SigningCertificateLineage getLineageFromInputFile(File inputLineageFile) {
try (RandomAccessFile f = new RandomAccessFile(inputLineageFile, "r")) {
if (f.length() < 4) {
throw CommandExecutionException.builder()
.withInternalMessage("The input file is not a valid lineage file.")
.build();
}
DataSource apk = DataSources.asDataSource(f);
int magicValue = apk.getByteBuffer(0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
if (magicValue == SigningCertificateLineage.MAGIC) {
return SigningCertificateLineage.readFromFile(inputLineageFile);
} else if (magicValue == ZIP_MAGIC) {
return SigningCertificateLineage.readFromApkFile(inputLineageFile);
} else {
throw CommandExecutionException.builder()
.withInternalMessage("The input file is not a valid lineage file.")
.build();
}
} catch (IOException | ApkFormatException | IllegalArgumentException e) {
throw CommandExecutionException.builder().withCause(e).build();
}
}

private static void populateSourceStampFromFlags(
Builder buildApksCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat
.getSigningConfiguration()
.ifPresent(
signingConfig ->
apkGenerationConfiguration.setMinimumV3SigningApiVersion(
signingConfig.getMinimumV3SigningApiVersion()));
apkGenerationConfiguration.setMinimumV3RotationApiVersion(
signingConfig.getMinimumV3RotationApiVersion()));

return apkGenerationConfiguration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.android.tools.build.bundletool.model.Password;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.utils.files.FileUtils;
import com.google.common.base.Ascii;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -211,7 +212,7 @@ public EnumFlag(String name, Class<T> enumType) {
@Override
protected T parse(String value) {
try {
return Enum.valueOf(enumType, value.toUpperCase());
return Enum.valueOf(enumType, Ascii.toUpperCase(value));
} catch (IllegalArgumentException e) {
throw new FlagParseException(
String.format(
Expand Down
95 changes: 14 additions & 81 deletions src/main/java/com/android/tools/build/bundletool/io/ApkSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,14 @@
*/
package com.android.tools.build.bundletool.io;

import static java.lang.Math.max;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import com.android.apksig.ApkSigner.SignerConfig;
import com.android.apksig.apk.ApkFormatException;
import com.android.tools.build.bundletool.commands.BuildApksModule.ApkSigningConfig;
import com.android.tools.build.bundletool.commands.BuildApksModule.StampSigningConfig;
import com.android.tools.build.bundletool.model.ModuleEntry;
import com.android.tools.build.bundletool.model.ModuleSplit;
import com.android.tools.build.bundletool.model.SigningConfiguration;
import com.android.tools.build.bundletool.model.WearApkLocator;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException;
import com.android.tools.build.bundletool.model.targeting.TargetingUtils;
import com.android.tools.build.bundletool.model.utils.Versions;
import com.android.tools.build.bundletool.model.version.Version;
import com.android.tools.build.bundletool.model.version.VersionGuardedFeature;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CheckReturnValue;
Expand All @@ -43,59 +34,43 @@
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Optional;
import javax.inject.Inject;

/** Signs APKs. */
class ApkSigner {

/** Name identifying uniquely the {@link SignerConfig} passed to the engine. */
private static final String SIGNER_CONFIG_NAME = "BNDLTOOL";

private final Optional<SigningConfiguration> optSigningConfig;
private final Optional<SigningConfiguration> optStampSigningConfig;
private final Version bundletoolVersion;
private final SigningConfigurationHelper signingConfigHelper;
private final TempDirectory tempDirectory;

@Inject
ApkSigner(
@ApkSigningConfig Optional<SigningConfiguration> signingConfig,
@StampSigningConfig Optional<SigningConfiguration> stampSigningConfig,
Version bundletoolVersion,
TempDirectory tempDirectory) {
this.optSigningConfig = signingConfig;
this.optStampSigningConfig = stampSigningConfig;
this.bundletoolVersion = bundletoolVersion;
ApkSigner(SigningConfigurationHelper signingConfigHelper, TempDirectory tempDirectory) {
this.signingConfigHelper = signingConfigHelper;
this.tempDirectory = tempDirectory;
}

public void signApk(Path apkPath, ModuleSplit split) {
if (!optSigningConfig.isPresent()) {
if (!signingConfigHelper.shouldSignGeneratedApks()) {
return;
}
SigningConfiguration signingConfiguration = optSigningConfig.get();

boolean signWithV1 = shouldSignWithV1Scheme(split);
boolean signWithV3 = shouldSignWithV3Scheme(split);
int minSdkVersion = split.getAndroidManifest().getEffectiveMinSdkVersion();

try (TempDirectory tempDirectory = new TempDirectory(getClass().getSimpleName())) {
Path signedApkPath = tempDirectory.getPath().resolve("signed.apk");
com.android.apksig.ApkSigner.Builder apkSigner =
new com.android.apksig.ApkSigner.Builder(
extractSignerConfigs(signingConfiguration, signWithV3))
signingConfigHelper.getSignerConfigsForSplit(split))
.setInputApk(apkPath.toFile())
.setOutputApk(signedApkPath.toFile())
.setV1SigningEnabled(signWithV1)
.setV1SigningEnabled(signingConfigHelper.shouldSignWithV1(split))
.setV2SigningEnabled(true)
.setV3SigningEnabled(signWithV3)
.setV3SigningEnabled(true)
.setOtherSignersSignaturesPreserved(false)
.setMinSdkVersion(minSdkVersion);
optStampSigningConfig.ifPresent(
stampConfig -> {
apkSigner.setSourceStampSignerConfig(
convertToApksigSignerConfig(stampConfig.getSignerConfig()));
});
.setMinSdkVersion(split.getAndroidManifest().getEffectiveMinSdkVersion());
signingConfigHelper
.getSigningCertificateLineageForSplit(split)
.ifPresent(apkSigner::setSigningCertificateLineage);
signingConfigHelper
.getSourceStampSignerConfig()
.ifPresent(apkSigner::setSourceStampSignerConfig);
apkSigner.build().sign();
Files.move(signedApkPath, apkPath, REPLACE_EXISTING);
} catch (IOException
Expand Down Expand Up @@ -148,46 +123,4 @@ private ModuleEntry signModuleEntry(ModuleSplit split, ModuleEntry entry) {
throw new UncheckedIOException(e);
}
}

private static ImmutableList<SignerConfig> extractSignerConfigs(
SigningConfiguration signingConfiguration, boolean signWithV3) {
if (!signWithV3) {
return ImmutableList.of(
convertToApksigSignerConfig(signingConfiguration.getSignerConfigForV1AndV2()));
}

ImmutableList.Builder<SignerConfig> signerConfigs = ImmutableList.builder();
signerConfigs.add(convertToApksigSignerConfig(signingConfiguration.getSignerConfig()));
return signerConfigs.build();
}

private static SignerConfig convertToApksigSignerConfig(
com.android.tools.build.bundletool.model.SignerConfig signerConfig) {
return new SignerConfig.Builder(
SIGNER_CONFIG_NAME, signerConfig.getPrivateKey(), signerConfig.getCertificates())
.build();
}

private boolean shouldSignWithV1Scheme(ModuleSplit split) {
return split.getAndroidManifest().getEffectiveMinSdkVersion() < Versions.ANDROID_N_API_VERSION
|| !VersionGuardedFeature.NO_V1_SIGNING_WHEN_POSSIBLE.enabledForVersion(bundletoolVersion);
}

private boolean shouldSignWithV3Scheme(ModuleSplit split) {
if (!optSigningConfig.isPresent()) {
// No v3 scheme rotation specified.
return false;
}

if (!optSigningConfig.get().getMinimumV3SigningApiVersion().isPresent()) {
// Caller specified rotations are fine to perform on all API levels.
return true;
}

int minimumV3signingApiVersion = optSigningConfig.get().getMinimumV3SigningApiVersion().get();
int minManifestSdkVersion = split.getAndroidManifest().getEffectiveMinSdkVersion();
int minApkTargetingSdkVersion =
TargetingUtils.getMinSdk(split.getApkTargeting().getSdkVersionTargeting());
return max(minManifestSdkVersion, minApkTargetingSdkVersion) >= minimumV3signingApiVersion;
}
}
Loading

0 comments on commit e6f6a86

Please sign in to comment.