Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Add V2 signing to local fb4a
Browse files Browse the repository at this point in the history
Summary:
Support V2 signing on local debug version of fb4a.
- Compile and include apksigner.jar from (Google's Android Open Source](https://fburl.com/0c6z5fof) to buck aosp respository
- Add an ApkSignerStep to ApkBuilder to support v1/v2/v3 signing on APK, using apksigner.jar

Reviewed By: styurin

fbshipit-source-id: 2bc1b7dd37
  • Loading branch information
Yuansong Feng authored and facebook-github-bot committed Aug 7, 2018
1 parent 1e2d7d4 commit cf9c2ad
Show file tree
Hide file tree
Showing 14 changed files with 464 additions and 14 deletions.
9 changes: 9 additions & 0 deletions .idea/libraries/apksig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
<include name="android/layoutlib-api-25.3.0.jar" />
<include name="android/sdk-common-25.3.0.jar" />
<include name="aopalliance/aopalliance.jar" />
<include name="aosp/apksig.jar" />
<include name="asm/asm*-6.0.jar" />
<include name="bazel/skylark-lang_deploy.jar" />
<include name="bundletool/bundle.jar" />
Expand Down
1 change: 1 addition & 0 deletions programs/classpaths
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ third-party/java/android/layoutlib-api-25.3.0.jar
third-party/java/android/sdk-common-25.3.0.jar
third-party/java/android/sdklib-25.3.0.jar
third-party/java/aopalliance/aopalliance.jar
third-party/java/aosp/apksig.jar
third-party/java/args4j/args4j-2.0.30.jar
third-party/java/asm/asm-debug-all-6.0_BETA.jar
third-party/java/closure-templates/soy-excluding-deps.jar
Expand Down
26 changes: 22 additions & 4 deletions src/com/facebook/buck/android/AndroidBinaryBuildable.java
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,9 @@ public ImmutableList<Step> getBuildSteps(
}

boolean applyRedex = redexOptions.isPresent();
Path apkPath = getFinalApkPath();
Path apkToAlign = apkToRedexAndAlign;
Path zipalignedApkPath = getZipalignedApkPath();
Path v2SignedApkPath = getFinalApkPath();

if (applyRedex) {
Path redexedApk = getRedexedApkPath();
Expand All @@ -337,9 +338,20 @@ public ImmutableList<Step> getBuildSteps(

steps.add(
new ZipalignStep(
getProjectFilesystem().getRootPath(), androidPlatformTarget, apkToAlign, apkPath));
getProjectFilesystem().getRootPath(),
androidPlatformTarget,
apkToAlign,
zipalignedApkPath));

buildableContext.recordArtifact(apkPath);
steps.add(
new ApkSignerStep(
getProjectFilesystem(),
zipalignedApkPath,
v2SignedApkPath,
keystoreProperties,
applyRedex));

buildableContext.recordArtifact(v2SignedApkPath);
return steps.build();
}

Expand Down Expand Up @@ -650,11 +662,17 @@ private Path getPathForNativeLibsAsAssets() {
getProjectFilesystem(), getBuildTarget(), "__native_libs_as_assets_%s__");
}

/** The APK at this path will be signed, but not zipaligned. */
/** The APK at this path will be jar signed, but not zipaligned. */
private Path getSignedApkPath() {
return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".signed.apk"));
}

/** The APK at this path will be zipaligned and jar signed. */
private Path getZipalignedApkPath() {
return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".zipaligned.apk"));
}

/** The APK at this path will be zipaligned and v2 signed. */
Path getFinalApkPath() {
return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".apk"));
}
Expand Down
164 changes: 164 additions & 0 deletions src/com/facebook/buck/android/ApkSignerStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright 2018-present Facebook, Inc.
*
* 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.facebook.buck.android;

import com.android.apksig.ApkSigner;
import com.android.sdklib.build.ApkCreationException;
import com.facebook.buck.io.filesystem.ProjectFilesystem;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.StepExecutionResults;
import com.google.common.base.Preconditions;
import java.io.File;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;

/** Use Google apksigner to v1/v2/v3 sign the final APK */
class ApkSignerStep implements Step {

private final ProjectFilesystem filesystem;
private final Path inputApkPath;
private final Path outputApkPath;
private final Supplier<KeystoreProperties> keystorePropertiesSupplier;
private final boolean isRedexBuild;

public ApkSignerStep(
ProjectFilesystem filesystem,
Path inputApkPath,
Path outputApkPath,
Supplier<KeystoreProperties> keystorePropertiesSupplier,
boolean isRedexBuild) {
this.filesystem = filesystem;
this.inputApkPath = inputApkPath;
this.outputApkPath = outputApkPath;
this.keystorePropertiesSupplier = keystorePropertiesSupplier;
this.isRedexBuild = isRedexBuild;
}

@Override
public StepExecutionResult execute(ExecutionContext context) {
try {
List<ApkSigner.SignerConfig> signerConfigs = getSignerConfigs();
File inputApkFile = filesystem.getPathForRelativePath(inputApkPath).toFile();
File outputApkFile = filesystem.getPathForRelativePath(outputApkPath).toFile();
signApkFile(inputApkFile, outputApkFile, signerConfigs);
} catch (Exception e) {
context.logError(e, "Error when signing APK at: %s.", outputApkPath);
return StepExecutionResults.ERROR;
}
return StepExecutionResults.SUCCESS;
}

/** Sign the APK using Google's {@link com.android.apksig.ApkSigner} */
private void signApkFile(
File inputApk, File outputApk, List<ApkSigner.SignerConfig> signerConfigs)
throws ApkCreationException {
ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signerConfigs);
// For non-redex build, apkSignerBuilder can look up minimum SDK version from
// AndroidManifest.xml. Redex build does not have AndroidManifest.xml, so we
// manually set it here.
if (isRedexBuild) {
apkSignerBuilder.setMinSdkVersion(1);
}
try {
apkSignerBuilder
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false)
.setInputApk(inputApk)
.setOutputApk(outputApk)
.build()
.sign();
} catch (Exception e) {
throw new ApkCreationException(e, "Failed to sign APK");
}
}

private List<ApkSigner.SignerConfig> getSignerConfigs() throws KeyStoreException {
KeystoreProperties keystoreProperties = keystorePropertiesSupplier.get();
Path keystorePath = keystoreProperties.getKeystore();
char[] keystorePassword = keystoreProperties.getStorepass().toCharArray();
String keyAlias = keystoreProperties.getAlias();
char[] keyPassword = keystoreProperties.getKeypass().toCharArray();
KeyStore keystore = loadKeyStore(keystorePath, keystorePassword);
PrivateKey key = loadPrivateKey(keystore, keyAlias, keyPassword);
List<X509Certificate> certs = loadCertificates(keystore, keyAlias);
ApkSigner.SignerConfig signerConfig =
new ApkSigner.SignerConfig.Builder("CERT", key, certs).build();
List<ApkSigner.SignerConfig> configs = new ArrayList<>(Arrays.asList(signerConfig));
return configs;
}

private KeyStore loadKeyStore(Path keystorePath, char[] keystorePassword)
throws KeyStoreException {
try {
String ksType = KeyStore.getDefaultType();
KeyStore keystore = KeyStore.getInstance(ksType);
keystore.load(filesystem.getInputStreamForRelativePath(keystorePath), keystorePassword);
return keystore;
} catch (Exception e) {
throw new KeyStoreException("Failed to load keystore from " + keystorePath);
}
}

private PrivateKey loadPrivateKey(KeyStore keystore, String keyAlias, char[] keyPassword)
throws KeyStoreException {
PrivateKey key;
try {
key = (PrivateKey) keystore.getKey(keyAlias, keyPassword);
// key can be null if alias/password is incorrect.
Preconditions.checkNotNull(key);
} catch (Exception e) {
throw new KeyStoreException(
"Failed to load private key \"" + keyAlias + "\" from " + keystore);
}
return key;
}

private List<X509Certificate> loadCertificates(KeyStore keystore, String keyAlias)
throws KeyStoreException {
Certificate[] certChain = keystore.getCertificateChain(keyAlias);
if ((certChain == null) || (certChain.length == 0)) {
throw new KeyStoreException(
keystore + " entry \"" + keyAlias + "\" does not contain certificates");
}
List<X509Certificate> certs = new ArrayList<>(certChain.length);
for (Certificate cert : certChain) {
certs.add((X509Certificate) cert);
}
return certs;
}

@Override
public String getShortName() {
return "apk_signer";
}

@Override
public String getDescription(ExecutionContext context) {
return getShortName();
}
}
1 change: 1 addition & 0 deletions src/com/facebook/buck/android/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ java_immutables_library(
"//src/com/facebook/buck/zip:zip",
"//third-party/java/android:sdklib",
"//third-party/java/aosp:aosp",
"//third-party/java/aosp:apksig",
"//third-party/java/asm:asm",
"//third-party/java/d8:d8",
"//third-party/java/dx:dx",
Expand Down
17 changes: 17 additions & 0 deletions test/com/facebook/buck/android/AndroidBinaryIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import com.android.apksig.ApkVerifier;
import com.facebook.buck.core.model.BuildTarget;
import com.facebook.buck.core.model.BuildTargetFactory;
import com.facebook.buck.core.model.impl.BuildTargetPaths;
Expand All @@ -46,6 +47,7 @@
import com.google.common.hash.Hashing;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
Expand Down Expand Up @@ -658,4 +660,19 @@ public void testProguardOutput() throws IOException {
assertThat(withoutAapt, containsString("-printmapping"));
assertThat(withoutAapt, CoreMatchers.not(containsString("#generated")));
}

@Test
public void testSimpleApkSignature() throws IOException {
Path apkPath = workspace.buildAndReturnOutput(SIMPLE_TARGET);
File apkFile = filesystem.getPathForRelativePath(apkPath).toFile();
ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(apkFile);
ApkVerifier.Result result;
try {
result = apkVerifierBuilder.build().verify();
} catch (Exception e) {
throw new IOException("Failed to determine APK's minimum supported platform version", e);
}
assertTrue(result.isVerifiedUsingV1Scheme());
assertTrue(result.isVerifiedUsingV2Scheme());
}
}
1 change: 1 addition & 0 deletions test/com/facebook/buck/android/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ java_test(
"//test/com/facebook/buck/jvm/java/testutil:testutil",
"//test/com/facebook/buck/testutil:testutil",
"//test/com/facebook/buck/testutil/integration:util",
"//third-party/java/aosp:apksig",
"//third-party/java/commons-compress:commons-compress",
"//third-party/java/guava:guava",
"//third-party/java/hamcrest:java-hamcrest",
Expand Down
27 changes: 19 additions & 8 deletions third-party/java/aosp/BUCK
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
java_library(required_for_source_only_abi = True,
name = "aosp",
srcs = glob(["src/**/*.java"]),
licenses = [
java_library(
required_for_source_only_abi=True,
name="aosp",
srcs=glob(["src/**/*.java"]),
licenses=[
"NOTICE",
"LICENSE",
],
visibility = [
"PUBLIC",
],
deps = [
visibility=["PUBLIC"],
deps=[
"//third-party/java/android:sdklib",
"//third-party/java/gson:gson",
"//third-party/java/guava:guava",
],
)

prebuilt_jar(
name="apksig",
binary_jar="apksig.jar",
source_jar="apksig-sources.jar",
licenses=[
"LICENSE",
],
visibility=["PUBLIC"],
deps=[],
)
Loading

0 comments on commit cf9c2ad

Please sign in to comment.