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

Commit cf9c2ad

Browse files
Yuansong Fengfacebook-github-bot
Yuansong Feng
authored andcommitted
Add V2 signing to local fb4a
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
1 parent 1e2d7d4 commit cf9c2ad

File tree

14 files changed

+464
-14
lines changed

14 files changed

+464
-14
lines changed

.idea/libraries/apksig.xml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.xml

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
<include name="android/layoutlib-api-25.3.0.jar" />
135135
<include name="android/sdk-common-25.3.0.jar" />
136136
<include name="aopalliance/aopalliance.jar" />
137+
<include name="aosp/apksig.jar" />
137138
<include name="asm/asm*-6.0.jar" />
138139
<include name="bazel/skylark-lang_deploy.jar" />
139140
<include name="bundletool/bundle.jar" />

programs/classpaths

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ third-party/java/android/layoutlib-api-25.3.0.jar
1212
third-party/java/android/sdk-common-25.3.0.jar
1313
third-party/java/android/sdklib-25.3.0.jar
1414
third-party/java/aopalliance/aopalliance.jar
15+
third-party/java/aosp/apksig.jar
1516
third-party/java/args4j/args4j-2.0.30.jar
1617
third-party/java/asm/asm-debug-all-6.0_BETA.jar
1718
third-party/java/closure-templates/soy-excluding-deps.jar

src/com/facebook/buck/android/AndroidBinaryBuildable.java

+22-4
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,9 @@ public ImmutableList<Step> getBuildSteps(
319319
}
320320

321321
boolean applyRedex = redexOptions.isPresent();
322-
Path apkPath = getFinalApkPath();
323322
Path apkToAlign = apkToRedexAndAlign;
323+
Path zipalignedApkPath = getZipalignedApkPath();
324+
Path v2SignedApkPath = getFinalApkPath();
324325

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

338339
steps.add(
339340
new ZipalignStep(
340-
getProjectFilesystem().getRootPath(), androidPlatformTarget, apkToAlign, apkPath));
341+
getProjectFilesystem().getRootPath(),
342+
androidPlatformTarget,
343+
apkToAlign,
344+
zipalignedApkPath));
341345

342-
buildableContext.recordArtifact(apkPath);
346+
steps.add(
347+
new ApkSignerStep(
348+
getProjectFilesystem(),
349+
zipalignedApkPath,
350+
v2SignedApkPath,
351+
keystoreProperties,
352+
applyRedex));
353+
354+
buildableContext.recordArtifact(v2SignedApkPath);
343355
return steps.build();
344356
}
345357

@@ -650,11 +662,17 @@ private Path getPathForNativeLibsAsAssets() {
650662
getProjectFilesystem(), getBuildTarget(), "__native_libs_as_assets_%s__");
651663
}
652664

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

670+
/** The APK at this path will be zipaligned and jar signed. */
671+
private Path getZipalignedApkPath() {
672+
return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".zipaligned.apk"));
673+
}
674+
675+
/** The APK at this path will be zipaligned and v2 signed. */
658676
Path getFinalApkPath() {
659677
return Paths.get(getUnsignedApkPath().replaceAll("\\.unsigned\\.apk$", ".apk"));
660678
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2018-present Facebook, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
* not use this file except in compliance with the License. You may obtain
6+
* a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.facebook.buck.android;
18+
19+
import com.android.apksig.ApkSigner;
20+
import com.android.sdklib.build.ApkCreationException;
21+
import com.facebook.buck.io.filesystem.ProjectFilesystem;
22+
import com.facebook.buck.step.ExecutionContext;
23+
import com.facebook.buck.step.Step;
24+
import com.facebook.buck.step.StepExecutionResult;
25+
import com.facebook.buck.step.StepExecutionResults;
26+
import com.google.common.base.Preconditions;
27+
import java.io.File;
28+
import java.nio.file.Path;
29+
import java.security.KeyStore;
30+
import java.security.KeyStoreException;
31+
import java.security.PrivateKey;
32+
import java.security.cert.Certificate;
33+
import java.security.cert.X509Certificate;
34+
import java.util.ArrayList;
35+
import java.util.Arrays;
36+
import java.util.List;
37+
import java.util.function.Supplier;
38+
39+
/** Use Google apksigner to v1/v2/v3 sign the final APK */
40+
class ApkSignerStep implements Step {
41+
42+
private final ProjectFilesystem filesystem;
43+
private final Path inputApkPath;
44+
private final Path outputApkPath;
45+
private final Supplier<KeystoreProperties> keystorePropertiesSupplier;
46+
private final boolean isRedexBuild;
47+
48+
public ApkSignerStep(
49+
ProjectFilesystem filesystem,
50+
Path inputApkPath,
51+
Path outputApkPath,
52+
Supplier<KeystoreProperties> keystorePropertiesSupplier,
53+
boolean isRedexBuild) {
54+
this.filesystem = filesystem;
55+
this.inputApkPath = inputApkPath;
56+
this.outputApkPath = outputApkPath;
57+
this.keystorePropertiesSupplier = keystorePropertiesSupplier;
58+
this.isRedexBuild = isRedexBuild;
59+
}
60+
61+
@Override
62+
public StepExecutionResult execute(ExecutionContext context) {
63+
try {
64+
List<ApkSigner.SignerConfig> signerConfigs = getSignerConfigs();
65+
File inputApkFile = filesystem.getPathForRelativePath(inputApkPath).toFile();
66+
File outputApkFile = filesystem.getPathForRelativePath(outputApkPath).toFile();
67+
signApkFile(inputApkFile, outputApkFile, signerConfigs);
68+
} catch (Exception e) {
69+
context.logError(e, "Error when signing APK at: %s.", outputApkPath);
70+
return StepExecutionResults.ERROR;
71+
}
72+
return StepExecutionResults.SUCCESS;
73+
}
74+
75+
/** Sign the APK using Google's {@link com.android.apksig.ApkSigner} */
76+
private void signApkFile(
77+
File inputApk, File outputApk, List<ApkSigner.SignerConfig> signerConfigs)
78+
throws ApkCreationException {
79+
ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signerConfigs);
80+
// For non-redex build, apkSignerBuilder can look up minimum SDK version from
81+
// AndroidManifest.xml. Redex build does not have AndroidManifest.xml, so we
82+
// manually set it here.
83+
if (isRedexBuild) {
84+
apkSignerBuilder.setMinSdkVersion(1);
85+
}
86+
try {
87+
apkSignerBuilder
88+
.setV1SigningEnabled(true)
89+
.setV2SigningEnabled(true)
90+
.setV3SigningEnabled(false)
91+
.setInputApk(inputApk)
92+
.setOutputApk(outputApk)
93+
.build()
94+
.sign();
95+
} catch (Exception e) {
96+
throw new ApkCreationException(e, "Failed to sign APK");
97+
}
98+
}
99+
100+
private List<ApkSigner.SignerConfig> getSignerConfigs() throws KeyStoreException {
101+
KeystoreProperties keystoreProperties = keystorePropertiesSupplier.get();
102+
Path keystorePath = keystoreProperties.getKeystore();
103+
char[] keystorePassword = keystoreProperties.getStorepass().toCharArray();
104+
String keyAlias = keystoreProperties.getAlias();
105+
char[] keyPassword = keystoreProperties.getKeypass().toCharArray();
106+
KeyStore keystore = loadKeyStore(keystorePath, keystorePassword);
107+
PrivateKey key = loadPrivateKey(keystore, keyAlias, keyPassword);
108+
List<X509Certificate> certs = loadCertificates(keystore, keyAlias);
109+
ApkSigner.SignerConfig signerConfig =
110+
new ApkSigner.SignerConfig.Builder("CERT", key, certs).build();
111+
List<ApkSigner.SignerConfig> configs = new ArrayList<>(Arrays.asList(signerConfig));
112+
return configs;
113+
}
114+
115+
private KeyStore loadKeyStore(Path keystorePath, char[] keystorePassword)
116+
throws KeyStoreException {
117+
try {
118+
String ksType = KeyStore.getDefaultType();
119+
KeyStore keystore = KeyStore.getInstance(ksType);
120+
keystore.load(filesystem.getInputStreamForRelativePath(keystorePath), keystorePassword);
121+
return keystore;
122+
} catch (Exception e) {
123+
throw new KeyStoreException("Failed to load keystore from " + keystorePath);
124+
}
125+
}
126+
127+
private PrivateKey loadPrivateKey(KeyStore keystore, String keyAlias, char[] keyPassword)
128+
throws KeyStoreException {
129+
PrivateKey key;
130+
try {
131+
key = (PrivateKey) keystore.getKey(keyAlias, keyPassword);
132+
// key can be null if alias/password is incorrect.
133+
Preconditions.checkNotNull(key);
134+
} catch (Exception e) {
135+
throw new KeyStoreException(
136+
"Failed to load private key \"" + keyAlias + "\" from " + keystore);
137+
}
138+
return key;
139+
}
140+
141+
private List<X509Certificate> loadCertificates(KeyStore keystore, String keyAlias)
142+
throws KeyStoreException {
143+
Certificate[] certChain = keystore.getCertificateChain(keyAlias);
144+
if ((certChain == null) || (certChain.length == 0)) {
145+
throw new KeyStoreException(
146+
keystore + " entry \"" + keyAlias + "\" does not contain certificates");
147+
}
148+
List<X509Certificate> certs = new ArrayList<>(certChain.length);
149+
for (Certificate cert : certChain) {
150+
certs.add((X509Certificate) cert);
151+
}
152+
return certs;
153+
}
154+
155+
@Override
156+
public String getShortName() {
157+
return "apk_signer";
158+
}
159+
160+
@Override
161+
public String getDescription(ExecutionContext context) {
162+
return getShortName();
163+
}
164+
}

src/com/facebook/buck/android/BUCK

+1
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ java_immutables_library(
489489
"//src/com/facebook/buck/zip:zip",
490490
"//third-party/java/android:sdklib",
491491
"//third-party/java/aosp:aosp",
492+
"//third-party/java/aosp:apksig",
492493
"//third-party/java/asm:asm",
493494
"//third-party/java/d8:d8",
494495
"//third-party/java/dx:dx",

test/com/facebook/buck/android/AndroidBinaryIntegrationTest.java

+17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.junit.Assert.assertThat;
2828
import static org.junit.Assert.assertTrue;
2929

30+
import com.android.apksig.ApkVerifier;
3031
import com.facebook.buck.core.model.BuildTarget;
3132
import com.facebook.buck.core.model.BuildTargetFactory;
3233
import com.facebook.buck.core.model.impl.BuildTargetPaths;
@@ -46,6 +47,7 @@
4647
import com.google.common.hash.Hashing;
4748
import java.io.BufferedReader;
4849
import java.io.ByteArrayOutputStream;
50+
import java.io.File;
4951
import java.io.IOException;
5052
import java.io.InputStream;
5153
import java.io.InputStreamReader;
@@ -658,4 +660,19 @@ public void testProguardOutput() throws IOException {
658660
assertThat(withoutAapt, containsString("-printmapping"));
659661
assertThat(withoutAapt, CoreMatchers.not(containsString("#generated")));
660662
}
663+
664+
@Test
665+
public void testSimpleApkSignature() throws IOException {
666+
Path apkPath = workspace.buildAndReturnOutput(SIMPLE_TARGET);
667+
File apkFile = filesystem.getPathForRelativePath(apkPath).toFile();
668+
ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(apkFile);
669+
ApkVerifier.Result result;
670+
try {
671+
result = apkVerifierBuilder.build().verify();
672+
} catch (Exception e) {
673+
throw new IOException("Failed to determine APK's minimum supported platform version", e);
674+
}
675+
assertTrue(result.isVerifiedUsingV1Scheme());
676+
assertTrue(result.isVerifiedUsingV2Scheme());
677+
}
661678
}

test/com/facebook/buck/android/BUCK

+1
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ java_test(
689689
"//test/com/facebook/buck/jvm/java/testutil:testutil",
690690
"//test/com/facebook/buck/testutil:testutil",
691691
"//test/com/facebook/buck/testutil/integration:util",
692+
"//third-party/java/aosp:apksig",
692693
"//third-party/java/commons-compress:commons-compress",
693694
"//third-party/java/guava:guava",
694695
"//third-party/java/hamcrest:java-hamcrest",

third-party/java/aosp/BUCK

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
java_library(required_for_source_only_abi = True,
2-
name = "aosp",
3-
srcs = glob(["src/**/*.java"]),
4-
licenses = [
1+
java_library(
2+
required_for_source_only_abi=True,
3+
name="aosp",
4+
srcs=glob(["src/**/*.java"]),
5+
licenses=[
56
"NOTICE",
7+
"LICENSE",
68
],
7-
visibility = [
8-
"PUBLIC",
9-
],
10-
deps = [
9+
visibility=["PUBLIC"],
10+
deps=[
1111
"//third-party/java/android:sdklib",
1212
"//third-party/java/gson:gson",
1313
"//third-party/java/guava:guava",
1414
],
1515
)
16+
17+
prebuilt_jar(
18+
name="apksig",
19+
binary_jar="apksig.jar",
20+
source_jar="apksig-sources.jar",
21+
licenses=[
22+
"LICENSE",
23+
],
24+
visibility=["PUBLIC"],
25+
deps=[],
26+
)

0 commit comments

Comments
 (0)