1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.apksigner;
18 
19 import com.android.apksig.ApkSigner;
20 import com.android.apksig.ApkVerifier;
21 import com.android.apksig.SigningCertificateLineage;
22 import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
23 import com.android.apksig.apk.ApkFormatException;
24 import com.android.apksig.apk.MinSdkVersionException;
25 import com.android.apksig.util.DataSource;
26 import com.android.apksig.util.DataSources;
27 
28 import org.conscrypt.OpenSSLProvider;
29 
30 import java.io.BufferedReader;
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStreamReader;
34 import java.io.PrintStream;
35 import java.io.RandomAccessFile;
36 import java.nio.ByteOrder;
37 import java.nio.charset.StandardCharsets;
38 import java.nio.file.Files;
39 import java.nio.file.StandardCopyOption;
40 import java.security.MessageDigest;
41 import java.security.NoSuchAlgorithmException;
42 import java.security.Provider;
43 import java.security.PublicKey;
44 import java.security.Security;
45 import java.security.cert.CertificateEncodingException;
46 import java.security.cert.X509Certificate;
47 import java.security.interfaces.DSAKey;
48 import java.security.interfaces.DSAParams;
49 import java.security.interfaces.ECKey;
50 import java.security.interfaces.RSAKey;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.List;
54 
55 /**
56  * Command-line tool for signing APKs and for checking whether an APK's signature are expected to
57  * verify on Android devices.
58  */
59 public class ApkSignerTool {
60 
61     private static final String VERSION = "0.9";
62     private static final String HELP_PAGE_GENERAL = "help.txt";
63     private static final String HELP_PAGE_SIGN = "help_sign.txt";
64     private static final String HELP_PAGE_VERIFY = "help_verify.txt";
65     private static final String HELP_PAGE_ROTATE = "help_rotate.txt";
66     private static final String HELP_PAGE_LINEAGE = "help_lineage.txt";
67 
68     private static MessageDigest sha256 = null;
69     private static MessageDigest sha1 = null;
70     private static MessageDigest md5 = null;
71 
72     public static final int ZIP_MAGIC = 0x04034b50;
73 
main(String[] params)74     public static void main(String[] params) throws Exception {
75         if ((params.length == 0) || ("--help".equals(params[0])) || ("-h".equals(params[0]))) {
76             printUsage(HELP_PAGE_GENERAL);
77             return;
78         } else if ("--version".equals(params[0])) {
79             System.out.println(VERSION);
80             return;
81         }
82 
83         addProviders();
84 
85         String cmd = params[0];
86         try {
87             if ("sign".equals(cmd)) {
88                 sign(Arrays.copyOfRange(params, 1, params.length));
89                 return;
90             } else if ("verify".equals(cmd)) {
91                 verify(Arrays.copyOfRange(params, 1, params.length));
92                 return;
93             } else if ("rotate".equals(cmd)) {
94                 rotate(Arrays.copyOfRange(params, 1, params.length));
95                 return;
96             } else if ("lineage".equals(cmd)) {
97                 lineage(Arrays.copyOfRange(params, 1, params.length));
98                 return;
99             } else if ("help".equals(cmd)) {
100                 printUsage(HELP_PAGE_GENERAL);
101                 return;
102             } else if ("version".equals(cmd)) {
103                 System.out.println(VERSION);
104                 return;
105             } else {
106                 throw new ParameterException(
107                         "Unsupported command: " + cmd + ". See --help for supported commands");
108             }
109         } catch (ParameterException | OptionsParser.OptionsException e) {
110             System.err.println(e.getMessage());
111             System.exit(1);
112             return;
113         }
114     }
115 
116     /**
117      * Adds additional security providers to add support for signature algorithms not covered by
118      * the default providers.
119      */
addProviders()120     private static void addProviders() {
121         try {
122             Security.addProvider(new OpenSSLProvider());
123         } catch (UnsatisfiedLinkError e) {
124             // This is expected if the library path does not include the native conscrypt library;
125             // the default providers support all but PSS algorithms.
126         }
127     }
128 
sign(String[] params)129     private static void sign(String[] params) throws Exception {
130         if (params.length == 0) {
131             printUsage(HELP_PAGE_SIGN);
132             return;
133         }
134 
135         File outputApk = null;
136         File inputApk = null;
137         boolean verbose = false;
138         boolean v1SigningEnabled = true;
139         boolean v2SigningEnabled = true;
140         boolean v3SigningEnabled = true;
141         boolean v4SigningEnabled = true;
142         boolean forceSourceStampOverwrite = false;
143         boolean verityEnabled = false;
144         boolean debuggableApkPermitted = true;
145         int minSdkVersion = 1;
146         boolean minSdkVersionSpecified = false;
147         int maxSdkVersion = Integer.MAX_VALUE;
148         List<SignerParams> signers = new ArrayList<>(1);
149         SignerParams signerParams = new SignerParams();
150         SignerParams sourceStampSignerParams = new SignerParams();
151         SigningCertificateLineage lineage = null;
152         List<ProviderInstallSpec> providers = new ArrayList<>();
153         ProviderInstallSpec providerParams = new ProviderInstallSpec();
154         OptionsParser optionsParser = new OptionsParser(params);
155         String optionName;
156         String optionOriginalForm = null;
157         boolean v4SigningFlagFound = false;
158         boolean sourceStampFlagFound = false;
159         while ((optionName = optionsParser.nextOption()) != null) {
160             optionOriginalForm = optionsParser.getOptionOriginalForm();
161             if (("help".equals(optionName)) || ("h".equals(optionName))) {
162                 printUsage(HELP_PAGE_SIGN);
163                 return;
164             } else if ("out".equals(optionName)) {
165                 outputApk = new File(optionsParser.getRequiredValue("Output file name"));
166             } else if ("in".equals(optionName)) {
167                 inputApk = new File(optionsParser.getRequiredValue("Input file name"));
168             } else if ("min-sdk-version".equals(optionName)) {
169                 minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level");
170                 minSdkVersionSpecified = true;
171             } else if ("max-sdk-version".equals(optionName)) {
172                 maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
173             } else if ("v1-signing-enabled".equals(optionName)) {
174                 v1SigningEnabled = optionsParser.getOptionalBooleanValue(true);
175             } else if ("v2-signing-enabled".equals(optionName)) {
176                 v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
177             } else if ("v3-signing-enabled".equals(optionName)) {
178                 v3SigningEnabled = optionsParser.getOptionalBooleanValue(true);
179             } else if ("v4-signing-enabled".equals(optionName)) {
180                 v4SigningEnabled = optionsParser.getOptionalBooleanValue(true);
181                 v4SigningFlagFound = true;
182             } else if ("force-stamp-overwrite".equals(optionName)) {
183                 forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true);
184             } else if ("verity-enabled".equals(optionName)) {
185                 verityEnabled = optionsParser.getOptionalBooleanValue(true);
186             } else if ("debuggable-apk-permitted".equals(optionName)) {
187                 debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true);
188             } else if ("next-signer".equals(optionName)) {
189                 if (!signerParams.isEmpty()) {
190                     signers.add(signerParams);
191                     signerParams = new SignerParams();
192                 }
193             } else if ("ks".equals(optionName)) {
194                 signerParams.setKeystoreFile(optionsParser.getRequiredValue("KeyStore file"));
195             } else if ("ks-key-alias".equals(optionName)) {
196                 signerParams.setKeystoreKeyAlias(
197                         optionsParser.getRequiredValue("KeyStore key alias"));
198             } else if ("ks-pass".equals(optionName)) {
199                 signerParams.setKeystorePasswordSpec(
200                         optionsParser.getRequiredValue("KeyStore password"));
201             } else if ("key-pass".equals(optionName)) {
202                 signerParams.setKeyPasswordSpec(optionsParser.getRequiredValue("Key password"));
203             } else if ("pass-encoding".equals(optionName)) {
204                 String charsetName =
205                         optionsParser.getRequiredValue("Password character encoding");
206                 try {
207                     signerParams.setPasswordCharset(
208                             PasswordRetriever.getCharsetByName(charsetName));
209                 } catch (IllegalArgumentException e) {
210                     throw new ParameterException(
211                             "Unsupported password character encoding requested using"
212                                     + " --pass-encoding: " + charsetName);
213                 }
214             } else if ("v1-signer-name".equals(optionName)) {
215                 signerParams.setV1SigFileBasename(
216                         optionsParser.getRequiredValue("JAR signature file basename"));
217             } else if ("ks-type".equals(optionName)) {
218                 signerParams.setKeystoreType(optionsParser.getRequiredValue("KeyStore type"));
219             } else if ("ks-provider-name".equals(optionName)) {
220                 signerParams.setKeystoreProviderName(
221                         optionsParser.getRequiredValue("JCA KeyStore Provider name"));
222             } else if ("ks-provider-class".equals(optionName)) {
223                 signerParams.setKeystoreProviderClass(
224                         optionsParser.getRequiredValue("JCA KeyStore Provider class name"));
225             } else if ("ks-provider-arg".equals(optionName)) {
226                 signerParams.setKeystoreProviderArg(
227                         optionsParser.getRequiredValue(
228                                 "JCA KeyStore Provider constructor argument"));
229             } else if ("key".equals(optionName)) {
230                 signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file"));
231             } else if ("cert".equals(optionName)) {
232                 signerParams.setCertFile(optionsParser.getRequiredValue("Certificate file"));
233             } else if ("lineage".equals(optionName)) {
234                 File lineageFile = new File(optionsParser.getRequiredValue("Lineage File"));
235                 lineage = getLineageFromInputFile(lineageFile);
236             } else if ("v".equals(optionName) || "verbose".equals(optionName)) {
237                 verbose = optionsParser.getOptionalBooleanValue(true);
238             } else if ("next-provider".equals(optionName)) {
239                 if (!providerParams.isEmpty()) {
240                     providers.add(providerParams);
241                     providerParams = new ProviderInstallSpec();
242                 }
243             } else if ("provider-class".equals(optionName)) {
244                 providerParams.className =
245                         optionsParser.getRequiredValue("JCA Provider class name");
246             } else if ("provider-arg".equals(optionName)) {
247                 providerParams.constructorParam =
248                         optionsParser.getRequiredValue("JCA Provider constructor argument");
249             } else if ("provider-pos".equals(optionName)) {
250                 providerParams.position =
251                         optionsParser.getRequiredIntValue("JCA Provider position");
252             } else if ("stamp-signer".equals(optionName)) {
253                 sourceStampFlagFound = true;
254                 sourceStampSignerParams = processSignerParams(optionsParser);
255             } else {
256                 throw new ParameterException(
257                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
258                                 + " options.");
259             }
260         }
261         if (!signerParams.isEmpty()) {
262             signers.add(signerParams);
263         }
264         signerParams = null;
265         if (!providerParams.isEmpty()) {
266             providers.add(providerParams);
267         }
268         providerParams = null;
269 
270         if (signers.isEmpty()) {
271             throw new ParameterException("At least one signer must be specified");
272         }
273 
274         params = optionsParser.getRemainingParams();
275         if (inputApk != null) {
276             // Input APK has been specified via preceding parameters. We don't expect any more
277             // parameters.
278             if (params.length > 0) {
279                 throw new ParameterException(
280                         "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]);
281             }
282         } else {
283             // Input APK has not been specified via preceding parameters. The next parameter is
284             // supposed to be the path to input APK.
285             if (params.length < 1) {
286                 throw new ParameterException("Missing input APK");
287             } else if (params.length > 1) {
288                 throw new ParameterException(
289                         "Unexpected parameter(s) after input APK (" + params[1] + ")");
290             }
291             inputApk = new File(params[0]);
292         }
293         if ((minSdkVersionSpecified) && (minSdkVersion > maxSdkVersion)) {
294             throw new ParameterException(
295                     "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion
296                             + ")");
297         }
298 
299         // Install additional JCA Providers
300         for (ProviderInstallSpec providerInstallSpec : providers) {
301             providerInstallSpec.installProvider();
302         }
303 
304         ApkSigner.SignerConfig sourceStampSignerConfig = null;
305         List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size());
306         int signerNumber = 0;
307         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
308             for (SignerParams signer : signers) {
309                 signerNumber++;
310                 signer.setName("signer #" + signerNumber);
311                 ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever);
312                 if (signerConfig == null) {
313                     return;
314                 }
315                 signerConfigs.add(signerConfig);
316             }
317             if (sourceStampFlagFound) {
318                 sourceStampSignerParams.setName("stamp signer");
319                 sourceStampSignerConfig =
320                         getSignerConfig(sourceStampSignerParams, passwordRetriever);
321                 if (sourceStampSignerConfig == null) {
322                     return;
323                 }
324             }
325         }
326 
327         if (outputApk == null) {
328             outputApk = inputApk;
329         }
330         File tmpOutputApk;
331         if (inputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) {
332             tmpOutputApk = File.createTempFile("apksigner", ".apk");
333             tmpOutputApk.deleteOnExit();
334         } else {
335             tmpOutputApk = outputApk;
336         }
337         ApkSigner.Builder apkSignerBuilder =
338                 new ApkSigner.Builder(signerConfigs)
339                         .setInputApk(inputApk)
340                         .setOutputApk(tmpOutputApk)
341                         .setOtherSignersSignaturesPreserved(false)
342                         .setV1SigningEnabled(v1SigningEnabled)
343                         .setV2SigningEnabled(v2SigningEnabled)
344                         .setV3SigningEnabled(v3SigningEnabled)
345                         .setV4SigningEnabled(v4SigningEnabled)
346                         .setForceSourceStampOverwrite(forceSourceStampOverwrite)
347                         .setVerityEnabled(verityEnabled)
348                         .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
349                         .setDebuggableApkPermitted(debuggableApkPermitted)
350                         .setSigningCertificateLineage(lineage);
351         if (minSdkVersionSpecified) {
352             apkSignerBuilder.setMinSdkVersion(minSdkVersion);
353         }
354         if (v4SigningEnabled) {
355             final File outputV4SignatureFile =
356                     new File(outputApk.getCanonicalPath() + ".idsig");
357             Files.deleteIfExists(outputV4SignatureFile.toPath());
358             apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
359         }
360         if (sourceStampSignerConfig != null) {
361             apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
362         }
363         ApkSigner apkSigner = apkSignerBuilder.build();
364         try {
365             apkSigner.sign();
366         } catch (MinSdkVersionException e) {
367             String msg = e.getMessage();
368             if (!msg.endsWith(".")) {
369                 msg += '.';
370             }
371             throw new MinSdkVersionException(
372                     "Failed to determine APK's minimum supported platform version"
373                             + ". Use --min-sdk-version to override",
374                     e);
375         }
376         if (!tmpOutputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) {
377             Files.move(
378                     tmpOutputApk.toPath(), outputApk.toPath(), StandardCopyOption.REPLACE_EXISTING);
379         }
380 
381         if (verbose) {
382             System.out.println("Signed");
383         }
384     }
385 
getSignerConfig( SignerParams signer, PasswordRetriever passwordRetriever)386     private static ApkSigner.SignerConfig getSignerConfig(
387             SignerParams signer, PasswordRetriever passwordRetriever) {
388         try {
389             signer.loadPrivateKeyAndCerts(passwordRetriever);
390         } catch (ParameterException e) {
391             System.err.println(
392                     "Failed to load signer \"" + signer.getName() + "\": " + e.getMessage());
393             System.exit(2);
394             return null;
395         } catch (Exception e) {
396             System.err.println("Failed to load signer \"" + signer.getName() + "\"");
397             e.printStackTrace();
398             System.exit(2);
399             return null;
400         }
401         String v1SigBasename;
402         if (signer.getV1SigFileBasename() != null) {
403             v1SigBasename = signer.getV1SigFileBasename();
404         } else if (signer.getKeystoreKeyAlias() != null) {
405             v1SigBasename = signer.getKeystoreKeyAlias();
406         } else if (signer.getKeyFile() != null) {
407             String keyFileName = new File(signer.getKeyFile()).getName();
408             int delimiterIndex = keyFileName.indexOf('.');
409             if (delimiterIndex == -1) {
410                 v1SigBasename = keyFileName;
411             } else {
412                 v1SigBasename = keyFileName.substring(0, delimiterIndex);
413             }
414         } else {
415             throw new RuntimeException("Neither KeyStore key alias nor private key file available");
416         }
417         ApkSigner.SignerConfig signerConfig =
418                 new ApkSigner.SignerConfig.Builder(
419                         v1SigBasename, signer.getPrivateKey(), signer.getCerts())
420                         .build();
421         return signerConfig;
422     }
423 
verify(String[] params)424     private static void verify(String[] params) throws Exception {
425         if (params.length == 0) {
426             printUsage(HELP_PAGE_VERIFY);
427             return;
428         }
429 
430         File inputApk = null;
431         int minSdkVersion = 1;
432         boolean minSdkVersionSpecified = false;
433         int maxSdkVersion = Integer.MAX_VALUE;
434         boolean maxSdkVersionSpecified = false;
435         boolean printCerts = false;
436         boolean verbose = false;
437         boolean warningsTreatedAsErrors = false;
438         File v4SignatureFile = null;
439         OptionsParser optionsParser = new OptionsParser(params);
440         String optionName;
441         String optionOriginalForm = null;
442         while ((optionName = optionsParser.nextOption()) != null) {
443             optionOriginalForm = optionsParser.getOptionOriginalForm();
444             if ("min-sdk-version".equals(optionName)) {
445                 minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level");
446                 minSdkVersionSpecified = true;
447             } else if ("max-sdk-version".equals(optionName)) {
448                 maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
449                 maxSdkVersionSpecified = true;
450             } else if ("print-certs".equals(optionName)) {
451                 printCerts = optionsParser.getOptionalBooleanValue(true);
452             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
453                 verbose = optionsParser.getOptionalBooleanValue(true);
454             } else if ("Werr".equals(optionName)) {
455                 warningsTreatedAsErrors = optionsParser.getOptionalBooleanValue(true);
456             } else if (("help".equals(optionName)) || ("h".equals(optionName))) {
457                 printUsage(HELP_PAGE_VERIFY);
458                 return;
459             } else if ("v4-signature-file".equals(optionName)) {
460                 v4SignatureFile = new File(optionsParser.getRequiredValue(
461                         "Input V4 Signature File"));
462             } else if ("in".equals(optionName)) {
463                 inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
464             } else {
465                 throw new ParameterException(
466                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
467                                 + " options.");
468             }
469         }
470         params = optionsParser.getRemainingParams();
471 
472         if (inputApk != null) {
473             // Input APK has been specified in preceding parameters. We don't expect any more
474             // parameters.
475             if (params.length > 0) {
476                 throw new ParameterException(
477                         "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]);
478             }
479         } else {
480             // Input APK has not been specified in preceding parameters. The next parameter is
481             // supposed to be the input APK.
482             if (params.length < 1) {
483                 throw new ParameterException("Missing APK");
484             } else if (params.length > 1) {
485                 throw new ParameterException(
486                         "Unexpected parameter(s) after APK (" + params[1] + ")");
487             }
488             inputApk = new File(params[0]);
489         }
490 
491         if ((minSdkVersionSpecified) && (maxSdkVersionSpecified)
492                 && (minSdkVersion > maxSdkVersion)) {
493             throw new ParameterException(
494                     "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion
495                             + ")");
496         }
497 
498         ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk);
499         if (minSdkVersionSpecified) {
500             apkVerifierBuilder.setMinCheckedPlatformVersion(minSdkVersion);
501         }
502         if (maxSdkVersionSpecified) {
503             apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion);
504         }
505         if (v4SignatureFile != null) {
506             if (!v4SignatureFile.exists()) {
507                 throw new ParameterException("V4 signature file does not exist: "
508                         + v4SignatureFile.getCanonicalPath());
509             }
510             apkVerifierBuilder.setV4SignatureFile(v4SignatureFile);
511         }
512 
513         ApkVerifier apkVerifier = apkVerifierBuilder.build();
514         ApkVerifier.Result result;
515         try {
516             result = apkVerifier.verify();
517         } catch (MinSdkVersionException e) {
518             String msg = e.getMessage();
519             if (!msg.endsWith(".")) {
520                 msg += '.';
521             }
522             throw new MinSdkVersionException(
523                     "Failed to determine APK's minimum supported platform version"
524                             + ". Use --min-sdk-version to override",
525                     e);
526         }
527         boolean verified = result.isVerified();
528 
529         boolean warningsEncountered = false;
530         if (verified) {
531             List<X509Certificate> signerCerts = result.getSignerCertificates();
532             if (verbose) {
533                 System.out.println("Verifies");
534                 System.out.println(
535                         "Verified using v1 scheme (JAR signing): "
536                                 + result.isVerifiedUsingV1Scheme());
537                 System.out.println(
538                         "Verified using v2 scheme (APK Signature Scheme v2): "
539                                 + result.isVerifiedUsingV2Scheme());
540                 System.out.println(
541                         "Verified using v3 scheme (APK Signature Scheme v3): "
542                                 + result.isVerifiedUsingV3Scheme());
543                 System.out.println(
544                         "Verified using v4 scheme (APK Signature Scheme v4): "
545                                 + result.isVerifiedUsingV4Scheme());
546                 System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
547                 System.out.println("Number of signers: " + signerCerts.size());
548             }
549             if (printCerts) {
550                 int signerNumber = 0;
551                 for (X509Certificate signerCert : signerCerts) {
552                     signerNumber++;
553                     printCertificate(signerCert, "Signer #" + signerNumber, verbose);
554                 }
555             }
556         } else {
557             System.err.println("DOES NOT VERIFY");
558         }
559 
560         for (ApkVerifier.IssueWithParams error : result.getErrors()) {
561             System.err.println("ERROR: " + error);
562         }
563 
564         @SuppressWarnings("resource") // false positive -- this resource is not opened here
565         PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out;
566         for (ApkVerifier.IssueWithParams warning : result.getWarnings()) {
567             warningsEncountered = true;
568             warningsOut.println("WARNING: " + warning);
569         }
570         for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
571             String signerName = signer.getName();
572             for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
573                 System.err.println("ERROR: JAR signer " + signerName + ": " + error);
574             }
575             for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
576                 warningsEncountered = true;
577                 warningsOut.println("WARNING: JAR signer " + signerName + ": " + warning);
578             }
579         }
580         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
581             String signerName = "signer #" + (signer.getIndex() + 1);
582             for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
583                 System.err.println(
584                         "ERROR: APK Signature Scheme v2 " + signerName + ": " + error);
585             }
586             for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
587                 warningsEncountered = true;
588                 warningsOut.println(
589                         "WARNING: APK Signature Scheme v2 " + signerName + ": " + warning);
590             }
591         }
592         for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
593             String signerName = "signer #" + (signer.getIndex() + 1);
594             for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
595                 System.err.println(
596                         "ERROR: APK Signature Scheme v3 " + signerName + ": " + error);
597             }
598             for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
599                 warningsEncountered = true;
600                 warningsOut.println(
601                         "WARNING: APK Signature Scheme v3 " + signerName + ": " + warning);
602             }
603         }
604 
605         ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
606         if (sourceStampInfo != null) {
607             for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
608                 System.err.println("ERROR: SourceStamp: " + error);
609             }
610             for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) {
611                 warningsOut.println("WARNING: SourceStamp: " + warning);
612             }
613         }
614 
615         if (!verified) {
616             System.exit(1);
617             return;
618         }
619         if ((warningsTreatedAsErrors) && (warningsEncountered)) {
620             System.exit(1);
621             return;
622         }
623     }
624 
rotate(String[] params)625     private static void rotate(String[] params) throws Exception {
626         if (params.length == 0) {
627             printUsage(HELP_PAGE_ROTATE);
628             return;
629         }
630 
631         File outputKeyLineage = null;
632         File inputKeyLineage = null;
633         boolean verbose = false;
634         SignerParams oldSignerParams = null;
635         SignerParams newSignerParams = null;
636         int minSdkVersion = 0;
637         List<ProviderInstallSpec> providers = new ArrayList<>();
638         ProviderInstallSpec providerParams = new ProviderInstallSpec();
639         OptionsParser optionsParser = new OptionsParser(params);
640         String optionName;
641         String optionOriginalForm = null;
642         while ((optionName = optionsParser.nextOption()) != null) {
643             optionOriginalForm = optionsParser.getOptionOriginalForm();
644             if (("help".equals(optionName)) || ("h".equals(optionName))) {
645                 printUsage(HELP_PAGE_ROTATE);
646                 return;
647             } else if ("out".equals(optionName)) {
648                 outputKeyLineage = new File(optionsParser.getRequiredValue("Output file name"));
649             } else if ("in".equals(optionName)) {
650                 inputKeyLineage = new File(optionsParser.getRequiredValue("Input file name"));
651             } else if ("old-signer".equals(optionName)) {
652                 oldSignerParams = processSignerParams(optionsParser);
653             } else if ("new-signer".equals(optionName)) {
654                 newSignerParams = processSignerParams(optionsParser);
655             } else if ("min-sdk-version".equals(optionName)) {
656                 minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level");
657             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
658                 verbose = optionsParser.getOptionalBooleanValue(true);
659             } else if ("next-provider".equals(optionName)) {
660                 if (!providerParams.isEmpty()) {
661                     providers.add(providerParams);
662                     providerParams = new ProviderInstallSpec();
663                 }
664             } else if ("provider-class".equals(optionName)) {
665                 providerParams.className =
666                         optionsParser.getRequiredValue("JCA Provider class name");
667             } else if ("provider-arg".equals(optionName)) {
668                 providerParams.constructorParam =
669                         optionsParser.getRequiredValue("JCA Provider constructor argument");
670             } else if ("provider-pos".equals(optionName)) {
671                 providerParams.position =
672                         optionsParser.getRequiredIntValue("JCA Provider position");
673             } else {
674                 throw new ParameterException(
675                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
676                                 + " options.");
677             }
678         }
679         if (!providerParams.isEmpty()) {
680             providers.add(providerParams);
681         }
682         providerParams = null;
683 
684         if (oldSignerParams.isEmpty()) {
685             throw new ParameterException("Signer parameters for old signer not present");
686         }
687 
688         if (newSignerParams.isEmpty()) {
689             throw new ParameterException("Signer parameters for new signer not present");
690         }
691 
692         if (outputKeyLineage == null) {
693             throw new ParameterException("Output lineage file parameter not present");
694         }
695 
696         params = optionsParser.getRemainingParams();
697         if (params.length > 0) {
698             throw new ParameterException(
699                     "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]);
700         }
701 
702 
703         // Install additional JCA Providers
704         for (ProviderInstallSpec providerInstallSpec : providers) {
705             providerInstallSpec.installProvider();
706         }
707 
708         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
709             // populate SignerConfig for old signer
710             oldSignerParams.setName("old signer");
711             loadPrivateKeyAndCerts(oldSignerParams, passwordRetriever);
712             SigningCertificateLineage.SignerConfig oldSignerConfig =
713                     new SigningCertificateLineage.SignerConfig.Builder(
714                             oldSignerParams.getPrivateKey(), oldSignerParams.getCerts().get(0))
715                             .build();
716 
717             // TOOD: don't require private key
718             newSignerParams.setName("new signer");
719             loadPrivateKeyAndCerts(newSignerParams, passwordRetriever);
720             SigningCertificateLineage.SignerConfig newSignerConfig =
721                     new SigningCertificateLineage.SignerConfig.Builder(
722                             newSignerParams.getPrivateKey(), newSignerParams.getCerts().get(0))
723                             .build();
724 
725             // ok we're all set up, let's rotate!
726             SigningCertificateLineage lineage;
727             if (inputKeyLineage != null) {
728                 // we already have history, add the new key to the end of it
729                 lineage = getLineageFromInputFile(inputKeyLineage);
730                 lineage.updateSignerCapabilities(
731                         oldSignerConfig, oldSignerParams.getSignerCapabilitiesBuilder().build());
732                 lineage =
733                         lineage.spawnDescendant(
734                                 oldSignerConfig,
735                                 newSignerConfig,
736                                 newSignerParams.getSignerCapabilitiesBuilder().build());
737             } else {
738                 // this is the first entry in our signing history, create a new one from the old and
739                 // new signer info
740                 lineage =
741                         new SigningCertificateLineage.Builder(oldSignerConfig, newSignerConfig)
742                                 .setMinSdkVersion(minSdkVersion)
743                                 .setOriginalCapabilities(
744                                         oldSignerParams.getSignerCapabilitiesBuilder().build())
745                                 .setNewCapabilities(
746                                         newSignerParams.getSignerCapabilitiesBuilder().build())
747                                 .build();
748             }
749             // and write out the result
750             lineage.writeToFile(outputKeyLineage);
751         }
752         if (verbose) {
753             System.out.println("Rotation entry generated.");
754         }
755     }
756 
lineage(String[] params)757     public static void lineage(String[] params) throws Exception {
758         if (params.length == 0) {
759             printUsage(HELP_PAGE_LINEAGE);
760             return;
761         }
762 
763         boolean verbose = false;
764         boolean printCerts = false;
765         boolean lineageUpdated = false;
766         File inputKeyLineage = null;
767         File outputKeyLineage = null;
768         String optionName;
769         OptionsParser optionsParser = new OptionsParser(params);
770         List<SignerParams> signers = new ArrayList<>(1);
771         while ((optionName = optionsParser.nextOption()) != null) {
772             if (("help".equals(optionName)) || ("h".equals(optionName))) {
773                 printUsage(HELP_PAGE_LINEAGE);
774                 return;
775             } else if ("in".equals(optionName)) {
776                 inputKeyLineage = new File(optionsParser.getRequiredValue("Input file name"));
777             } else if ("out".equals(optionName)) {
778                 outputKeyLineage = new File(optionsParser.getRequiredValue("Output file name"));
779             } else if ("signer".equals(optionName)) {
780                 SignerParams signerParams = processSignerParams(optionsParser);
781                 signers.add(signerParams);
782             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
783                 verbose = optionsParser.getOptionalBooleanValue(true);
784             } else if ("print-certs".equals(optionName)) {
785                 printCerts = optionsParser.getOptionalBooleanValue(true);
786             } else {
787                 throw new ParameterException(
788                         "Unsupported option: " + optionsParser.getOptionOriginalForm()
789                                 + ". See --help for supported options.");
790             }
791         }
792         if (inputKeyLineage == null) {
793             throw new ParameterException("Input lineage file parameter not present");
794         }
795         SigningCertificateLineage lineage = getLineageFromInputFile(inputKeyLineage);
796 
797         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
798             for (int i = 0; i < signers.size(); i++) {
799                 SignerParams signerParams = signers.get(i);
800                 signerParams.setName("signer #" + (i + 1));
801                 loadPrivateKeyAndCerts(signerParams, passwordRetriever);
802                 SigningCertificateLineage.SignerConfig signerConfig =
803                         new SigningCertificateLineage.SignerConfig.Builder(
804                                 signerParams.getPrivateKey(), signerParams.getCerts().get(0))
805                                 .build();
806                 try {
807                     // since only the caller specified capabilities will be updated a direct
808                     // comparison between the original capabilities of the signer and the
809                     // signerCapabilitiesBuilder object with potential default values is not
810                     // possible. Instead the capabilities should be updated first, then the new
811                     // capabilities can be compared against the original to determine if the
812                     // lineage has been updated and needs to be written out to a file.
813                     SignerCapabilities origCapabilities = lineage.getSignerCapabilities(
814                             signerConfig);
815                     lineage.updateSignerCapabilities(
816                             signerConfig, signerParams.getSignerCapabilitiesBuilder().build());
817                     SignerCapabilities newCapabilities = lineage.getSignerCapabilities(
818                             signerConfig);
819                     if (origCapabilities.equals(newCapabilities)) {
820                         if (verbose) {
821                             System.out.println(
822                                     "The provided signer capabilities for "
823                                             + signerParams.getName()
824                                             + " are unchanged.");
825                         }
826                     } else {
827                         lineageUpdated = true;
828                         if (verbose) {
829                             System.out.println(
830                                     "Updated signer capabilities for " + signerParams.getName()
831                                             + ".");
832                         }
833                     }
834                 } catch (IllegalArgumentException e) {
835                     throw new ParameterException(
836                             "The signer " + signerParams.getName()
837                                     + " was not found in the specified lineage.");
838                 }
839             }
840         }
841         if (printCerts) {
842             List<X509Certificate> signingCerts = lineage.getCertificatesInLineage();
843             for (int i = 0; i < signingCerts.size(); i++) {
844                 X509Certificate signerCert = signingCerts.get(i);
845                 SignerCapabilities signerCapabilities = lineage.getSignerCapabilities(signerCert);
846                 printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose);
847                 printCapabilities(signerCapabilities);
848             }
849         }
850         if (lineageUpdated) {
851             if (outputKeyLineage != null) {
852                 lineage.writeToFile(outputKeyLineage);
853                 if (verbose) {
854                     System.out.println("Updated lineage saved to " + outputKeyLineage + ".");
855                 }
856             } else {
857                 throw new ParameterException(
858                         "The lineage was modified but an output file for the lineage was not "
859                                 + "specified");
860             }
861         }
862     }
863 
864     /**
865      * Extracts the Signing Certificate Lineage from the provided lineage or APK file.
866      */
getLineageFromInputFile(File inputLineageFile)867     private static SigningCertificateLineage getLineageFromInputFile(File inputLineageFile)
868             throws ParameterException {
869         try (RandomAccessFile f = new RandomAccessFile(inputLineageFile, "r")) {
870             if (f.length() < 4) {
871                 throw new ParameterException("The input file is not a valid lineage file.");
872             }
873             DataSource apk = DataSources.asDataSource(f);
874             int magicValue = apk.getByteBuffer(0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
875             if (magicValue == SigningCertificateLineage.MAGIC) {
876                 return SigningCertificateLineage.readFromFile(inputLineageFile);
877             } else if (magicValue == ZIP_MAGIC) {
878                 return SigningCertificateLineage.readFromApkFile(inputLineageFile);
879             } else {
880                 throw new ParameterException("The input file is not a valid lineage file.");
881             }
882         } catch (IOException | ApkFormatException | IllegalArgumentException e) {
883             throw new ParameterException(e.getMessage());
884         }
885     }
886 
processSignerParams(OptionsParser optionsParser)887     private static SignerParams processSignerParams(OptionsParser optionsParser)
888             throws OptionsParser.OptionsException, ParameterException {
889         SignerParams signerParams = new SignerParams();
890         String optionName;
891         while ((optionName = optionsParser.nextOption()) != null) {
892             if ("ks".equals(optionName)) {
893                 signerParams.setKeystoreFile(optionsParser.getRequiredValue("KeyStore file"));
894             } else if ("ks-key-alias".equals(optionName)) {
895                 signerParams.setKeystoreKeyAlias(
896                         optionsParser.getRequiredValue("KeyStore key alias"));
897             } else if ("ks-pass".equals(optionName)) {
898                 signerParams.setKeystorePasswordSpec(
899                         optionsParser.getRequiredValue("KeyStore password"));
900             } else if ("key-pass".equals(optionName)) {
901                 signerParams.setKeyPasswordSpec(optionsParser.getRequiredValue("Key password"));
902             } else if ("pass-encoding".equals(optionName)) {
903                 String charsetName =
904                         optionsParser.getRequiredValue("Password character encoding");
905                 try {
906                     signerParams.setPasswordCharset(
907                             PasswordRetriever.getCharsetByName(charsetName));
908                 } catch (IllegalArgumentException e) {
909                     throw new ParameterException(
910                             "Unsupported password character encoding requested using"
911                                     + " --pass-encoding: " + charsetName);
912                 }
913             } else if ("ks-type".equals(optionName)) {
914                 signerParams.setKeystoreType(optionsParser.getRequiredValue("KeyStore type"));
915             } else if ("ks-provider-name".equals(optionName)) {
916                 signerParams.setKeystoreProviderName(
917                         optionsParser.getRequiredValue("JCA KeyStore Provider name"));
918             } else if ("ks-provider-class".equals(optionName)) {
919                 signerParams.setKeystoreProviderClass(
920                         optionsParser.getRequiredValue("JCA KeyStore Provider class name"));
921             } else if ("ks-provider-arg".equals(optionName)) {
922                 signerParams.setKeystoreProviderArg(
923                         optionsParser.getRequiredValue(
924                                 "JCA KeyStore Provider constructor argument"));
925             } else if ("key".equals(optionName)) {
926                 signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file"));
927             } else if ("cert".equals(optionName)) {
928                 signerParams.setCertFile(optionsParser.getRequiredValue("Certificate file"));
929             } else if ("set-installed-data".equals(optionName)) {
930                 signerParams
931                         .getSignerCapabilitiesBuilder()
932                         .setInstalledData(optionsParser.getOptionalBooleanValue(true));
933             } else if ("set-shared-uid".equals(optionName)) {
934                 signerParams
935                         .getSignerCapabilitiesBuilder()
936                         .setSharedUid(optionsParser.getOptionalBooleanValue(true));
937             } else if ("set-permission".equals(optionName)) {
938                 signerParams
939                         .getSignerCapabilitiesBuilder()
940                         .setPermission(optionsParser.getOptionalBooleanValue(true));
941             } else if ("set-rollback".equals(optionName)) {
942                 signerParams
943                         .getSignerCapabilitiesBuilder()
944                         .setRollback(optionsParser.getOptionalBooleanValue(true));
945             } else if ("set-auth".equals(optionName)) {
946                 signerParams
947                         .getSignerCapabilitiesBuilder()
948                         .setAuth(optionsParser.getOptionalBooleanValue(true));
949             } else {
950                 // not a signer option, reset optionsParser and let caller deal with it
951                 optionsParser.putOption();
952                 break;
953             }
954         }
955 
956         if (signerParams.isEmpty()) {
957             throw new ParameterException("Signer specified without arguments");
958         }
959         return signerParams;
960     }
961 
printUsage(String page)962     private static void printUsage(String page) {
963         try (BufferedReader in =
964                 new BufferedReader(
965                         new InputStreamReader(
966                                 ApkSignerTool.class.getResourceAsStream(page),
967                                 StandardCharsets.UTF_8))) {
968             String line;
969             while ((line = in.readLine()) != null) {
970                 System.out.println(line);
971             }
972         } catch (IOException e) {
973             throw new RuntimeException("Failed to read " + page + " resource");
974         }
975     }
976 
977     /**
978      * Prints details from the provided certificate to stdout.
979      *
980      * @param cert    the certificate to be displayed.
981      * @param name    the name to be used to identify the certificate.
982      * @param verbose boolean indicating whether public key details from the certificate should be
983      *                displayed.
984      *
985      * @throws NoSuchAlgorithmException     if an instance of MD5, SHA-1, or SHA-256 cannot be
986      *                                      obtained.
987      * @throws CertificateEncodingException if an error is encountered when encoding the
988      *                                      certificate.
989      */
printCertificate(X509Certificate cert, String name, boolean verbose)990     public static void printCertificate(X509Certificate cert, String name, boolean verbose)
991             throws NoSuchAlgorithmException, CertificateEncodingException {
992         if (cert == null) {
993             throw new NullPointerException("cert == null");
994         }
995         if (sha256 == null || sha1 == null || md5 == null) {
996             sha256 = MessageDigest.getInstance("SHA-256");
997             sha1 = MessageDigest.getInstance("SHA-1");
998             md5 = MessageDigest.getInstance("MD5");
999         }
1000         System.out.println(name + " certificate DN: " + cert.getSubjectDN());
1001         byte[] encodedCert = cert.getEncoded();
1002         System.out.println(name + " certificate SHA-256 digest: " + HexEncoding.encode(
1003                 sha256.digest(encodedCert)));
1004         System.out.println(name + " certificate SHA-1 digest: " + HexEncoding.encode(
1005                 sha1.digest(encodedCert)));
1006         System.out.println(
1007                 name + " certificate MD5 digest: " + HexEncoding.encode(md5.digest(encodedCert)));
1008         if (verbose) {
1009             PublicKey publicKey = cert.getPublicKey();
1010             System.out.println(name + " key algorithm: " + publicKey.getAlgorithm());
1011             int keySize = -1;
1012             if (publicKey instanceof RSAKey) {
1013                 keySize = ((RSAKey) publicKey).getModulus().bitLength();
1014             } else if (publicKey instanceof ECKey) {
1015                 keySize = ((ECKey) publicKey).getParams()
1016                         .getOrder().bitLength();
1017             } else if (publicKey instanceof DSAKey) {
1018                 // DSA parameters may be inherited from the certificate. We
1019                 // don't handle this case at the moment.
1020                 DSAParams dsaParams = ((DSAKey) publicKey).getParams();
1021                 if (dsaParams != null) {
1022                     keySize = dsaParams.getP().bitLength();
1023                 }
1024             }
1025             System.out.println(
1026                     name + " key size (bits): " + ((keySize != -1) ? String.valueOf(keySize)
1027                             : "n/a"));
1028             byte[] encodedKey = publicKey.getEncoded();
1029             System.out.println(name + " public key SHA-256 digest: " + HexEncoding.encode(
1030                     sha256.digest(encodedKey)));
1031             System.out.println(name + " public key SHA-1 digest: " + HexEncoding.encode(
1032                     sha1.digest(encodedKey)));
1033             System.out.println(
1034                     name + " public key MD5 digest: " + HexEncoding.encode(md5.digest(encodedKey)));
1035         }
1036     }
1037 
1038     /**
1039      * Prints the capabilities of the provided object to stdout. Each of the potential
1040      * capabilities is displayed along with a boolean indicating whether this object has
1041      * that capability.
1042      */
printCapabilities(SignerCapabilities capabilities)1043     public static void printCapabilities(SignerCapabilities capabilities) {
1044         System.out.println("Has installed data capability: " + capabilities.hasInstalledData());
1045         System.out.println("Has shared UID capability    : " + capabilities.hasSharedUid());
1046         System.out.println("Has permission capability    : " + capabilities.hasPermission());
1047         System.out.println("Has rollback capability      : " + capabilities.hasRollback());
1048         System.out.println("Has auth capability          : " + capabilities.hasAuth());
1049     }
1050 
1051     private static class ProviderInstallSpec {
1052         String className;
1053         String constructorParam;
1054         Integer position;
1055 
isEmpty()1056         private boolean isEmpty() {
1057             return (className == null) && (constructorParam == null) && (position == null);
1058         }
1059 
installProvider()1060         private void installProvider() throws Exception {
1061             if (className == null) {
1062                 throw new ParameterException(
1063                         "JCA Provider class name (--provider-class) must be specified");
1064             }
1065 
1066             Class<?> providerClass = Class.forName(className);
1067             if (!Provider.class.isAssignableFrom(providerClass)) {
1068                 throw new ParameterException(
1069                         "JCA Provider class " + providerClass + " not subclass of "
1070                                 + Provider.class.getName());
1071             }
1072             Provider provider;
1073             if (constructorParam != null) {
1074                 // Single-arg Provider constructor
1075                 provider =
1076                         (Provider) providerClass.getConstructor(String.class)
1077                                 .newInstance(constructorParam);
1078             } else {
1079                 // No-arg Provider constructor
1080                 provider = (Provider) providerClass.getConstructor().newInstance();
1081             }
1082 
1083             if (position == null) {
1084                 Security.addProvider(provider);
1085             } else {
1086                 Security.insertProviderAt(provider, position);
1087             }
1088         }
1089     }
1090 
1091     /**
1092      * Loads the private key and certificates from either the specified keystore or files specified
1093      * in the signer params using the provided passwordRetriever.
1094      *
1095      * @throws ParameterException if any errors are encountered when attempting to load
1096      *                            the private key and certificates.
1097      */
loadPrivateKeyAndCerts(SignerParams params, PasswordRetriever passwordRetriever)1098     private static void loadPrivateKeyAndCerts(SignerParams params,
1099             PasswordRetriever passwordRetriever) throws ParameterException {
1100         try {
1101             params.loadPrivateKeyAndCerts(passwordRetriever);
1102             if (params.getKeystoreKeyAlias() != null) {
1103                 params.setName(params.getKeystoreKeyAlias());
1104             } else if (params.getKeyFile() != null) {
1105                 String keyFileName = new File(params.getKeyFile()).getName();
1106                 int delimiterIndex = keyFileName.indexOf('.');
1107                 if (delimiterIndex == -1) {
1108                     params.setName(keyFileName);
1109                 } else {
1110                     params.setName(keyFileName.substring(0, delimiterIndex));
1111                 }
1112             } else {
1113                 throw new RuntimeException(
1114                         "Neither KeyStore key alias nor private key file available for "
1115                                 + params.getName());
1116             }
1117         } catch (ParameterException e) {
1118             throw new ParameterException(
1119                     "Failed to load signer \"" + params.getName() + "\":" + e.getMessage());
1120         } catch (Exception e) {
1121             e.printStackTrace();
1122             throw new ParameterException("Failed to load signer \"" + params.getName() + "\"");
1123         }
1124     }
1125 }
1126