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