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.apk.MinSdkVersionException;
22 import java.io.BufferedReader;
23 import java.io.ByteArrayOutputStream;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.OutputStream;
30 import java.io.PrintStream;
31 import java.nio.charset.Charset;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.Files;
34 import java.nio.file.StandardCopyOption;
35 import java.security.InvalidKeyException;
36 import java.security.Key;
37 import java.security.KeyFactory;
38 import java.security.KeyStore;
39 import java.security.KeyStoreException;
40 import java.security.MessageDigest;
41 import java.security.NoSuchAlgorithmException;
42 import java.security.PrivateKey;
43 import java.security.Provider;
44 import java.security.PublicKey;
45 import java.security.Security;
46 import java.security.UnrecoverableKeyException;
47 import java.security.cert.Certificate;
48 import java.security.cert.CertificateFactory;
49 import java.security.cert.X509Certificate;
50 import java.security.interfaces.DSAKey;
51 import java.security.interfaces.DSAParams;
52 import java.security.interfaces.ECKey;
53 import java.security.interfaces.RSAKey;
54 import java.security.spec.InvalidKeySpecException;
55 import java.security.spec.PKCS8EncodedKeySpec;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collection;
59 import java.util.Enumeration;
60 import java.util.List;
61 import javax.crypto.EncryptedPrivateKeyInfo;
62 import javax.crypto.SecretKey;
63 import javax.crypto.SecretKeyFactory;
64 import javax.crypto.spec.PBEKeySpec;
65 
66 /**
67  * Command-line tool for signing APKs and for checking whether an APK's signature are expected to
68  * verify on Android devices.
69  */
70 public class ApkSignerTool {
71 
72     private static final String VERSION = "0.8";
73     private static final String HELP_PAGE_GENERAL = "help.txt";
74     private static final String HELP_PAGE_SIGN = "help_sign.txt";
75     private static final String HELP_PAGE_VERIFY = "help_verify.txt";
76 
main(String[] params)77     public static void main(String[] params) throws Exception {
78         if ((params.length == 0) || ("--help".equals(params[0])) || ("-h".equals(params[0]))) {
79             printUsage(HELP_PAGE_GENERAL);
80             return;
81         } else if ("--version".equals(params[0])) {
82             System.out.println(VERSION);
83             return;
84         }
85 
86         String cmd = params[0];
87         try {
88             if ("sign".equals(cmd)) {
89                 sign(Arrays.copyOfRange(params, 1, params.length));
90                 return;
91             } else if ("verify".equals(cmd)) {
92                 verify(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 debuggableApkPermitted = true;
123         int minSdkVersion = 1;
124         boolean minSdkVersionSpecified = false;
125         int maxSdkVersion = Integer.MAX_VALUE;
126         List<SignerParams> signers = new ArrayList<>(1);
127         SignerParams signerParams = new SignerParams();
128         List<ProviderInstallSpec> providers = new ArrayList<>();
129         ProviderInstallSpec providerParams = new ProviderInstallSpec();
130         OptionsParser optionsParser = new OptionsParser(params);
131         String optionName;
132         String optionOriginalForm = null;
133         while ((optionName = optionsParser.nextOption()) != null) {
134             optionOriginalForm = optionsParser.getOptionOriginalForm();
135             if (("help".equals(optionName)) || ("h".equals(optionName))) {
136                 printUsage(HELP_PAGE_SIGN);
137                 return;
138             } else if ("out".equals(optionName)) {
139                 outputApk = new File(optionsParser.getRequiredValue("Output file name"));
140             } else if ("in".equals(optionName)) {
141                 inputApk = new File(optionsParser.getRequiredValue("Input file name"));
142             } else if ("min-sdk-version".equals(optionName)) {
143                 minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level");
144                 minSdkVersionSpecified = true;
145             } else if ("max-sdk-version".equals(optionName)) {
146                 maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
147             } else if ("v1-signing-enabled".equals(optionName)) {
148                 v1SigningEnabled = optionsParser.getOptionalBooleanValue(true);
149             } else if ("v2-signing-enabled".equals(optionName)) {
150                 v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
151             } else if ("debuggable-apk-permitted".equals(optionName)) {
152                 debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true);
153             } else if ("next-signer".equals(optionName)) {
154                 if (!signerParams.isEmpty()) {
155                     signers.add(signerParams);
156                     signerParams = new SignerParams();
157                 }
158             } else if ("ks".equals(optionName)) {
159                 signerParams.keystoreFile = optionsParser.getRequiredValue("KeyStore file");
160             } else if ("ks-key-alias".equals(optionName)) {
161                 signerParams.keystoreKeyAlias =
162                         optionsParser.getRequiredValue("KeyStore key alias");
163             } else if ("ks-pass".equals(optionName)) {
164                 signerParams.keystorePasswordSpec =
165                         optionsParser.getRequiredValue("KeyStore password");
166             } else if ("key-pass".equals(optionName)) {
167                 signerParams.keyPasswordSpec = optionsParser.getRequiredValue("Key password");
168             } else if ("pass-encoding".equals(optionName)) {
169                 String charsetName =
170                         optionsParser.getRequiredValue("Password character encoding");
171                 try {
172                     signerParams.passwordCharset = PasswordRetriever.getCharsetByName(charsetName);
173                 } catch (IllegalArgumentException e) {
174                     throw new ParameterException(
175                             "Unsupported password character encoding requested using"
176                                     + " --pass-encoding: " + charsetName);
177                 }
178             } else if ("v1-signer-name".equals(optionName)) {
179                 signerParams.v1SigFileBasename =
180                         optionsParser.getRequiredValue("JAR signature file basename");
181             } else if ("ks-type".equals(optionName)) {
182                 signerParams.keystoreType = optionsParser.getRequiredValue("KeyStore type");
183             } else if ("ks-provider-name".equals(optionName)) {
184                 signerParams.keystoreProviderName =
185                         optionsParser.getRequiredValue("JCA KeyStore Provider name");
186             } else if ("ks-provider-class".equals(optionName)) {
187                 signerParams.keystoreProviderClass =
188                         optionsParser.getRequiredValue("JCA KeyStore Provider class name");
189             } else if ("ks-provider-arg".equals(optionName)) {
190                 signerParams.keystoreProviderArg =
191                         optionsParser.getRequiredValue(
192                                 "JCA KeyStore Provider constructor argument");
193             } else if ("key".equals(optionName)) {
194                 signerParams.keyFile = optionsParser.getRequiredValue("Private key file");
195             } else if ("cert".equals(optionName)) {
196                 signerParams.certFile = optionsParser.getRequiredValue("Certificate file");
197             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
198                 verbose = optionsParser.getOptionalBooleanValue(true);
199             } else if ("next-provider".equals(optionName)) {
200                 if (!providerParams.isEmpty()) {
201                     providers.add(providerParams);
202                     providerParams = new ProviderInstallSpec();
203                 }
204             } else if ("provider-class".equals(optionName)) {
205                 providerParams.className =
206                         optionsParser.getRequiredValue("JCA Provider class name");
207             } else if ("provider-arg".equals(optionName)) {
208                 providerParams.constructorParam =
209                         optionsParser.getRequiredValue("JCA Provider constructor argument");
210             } else if ("provider-pos".equals(optionName)) {
211                 providerParams.position =
212                         optionsParser.getRequiredIntValue("JCA Provider position");
213             } else {
214                 throw new ParameterException(
215                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
216                                 + " options.");
217             }
218         }
219         if (!signerParams.isEmpty()) {
220             signers.add(signerParams);
221         }
222         signerParams = null;
223         if (!providerParams.isEmpty()) {
224             providers.add(providerParams);
225         }
226         providerParams = null;
227 
228         if (signers.isEmpty()) {
229             throw new ParameterException("At least one signer must be specified");
230         }
231 
232         params = optionsParser.getRemainingParams();
233         if (inputApk != null) {
234             // Input APK has been specified via preceding parameters. We don't expect any more
235             // parameters.
236             if (params.length > 0) {
237                 throw new ParameterException(
238                         "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]);
239             }
240         } else {
241             // Input APK has not been specified via preceding parameters. The next parameter is
242             // supposed to be the path to input APK.
243             if (params.length < 1) {
244                 throw new ParameterException("Missing input APK");
245             } else if (params.length > 1) {
246                 throw new ParameterException(
247                         "Unexpected parameter(s) after input APK (" + params[1] + ")");
248             }
249             inputApk = new File(params[0]);
250         }
251         if ((minSdkVersionSpecified) && (minSdkVersion > maxSdkVersion)) {
252             throw new ParameterException(
253                     "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion
254                             + ")");
255         }
256 
257         // Install additional JCA Providers
258         for (ProviderInstallSpec providerInstallSpec : providers) {
259             providerInstallSpec.installProvider();
260         }
261 
262         List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size());
263         int signerNumber = 0;
264         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
265             for (SignerParams signer : signers) {
266                 signerNumber++;
267                 signer.name = "signer #" + signerNumber;
268                 try {
269                     signer.loadPrivateKeyAndCerts(passwordRetriever);
270                 } catch (ParameterException e) {
271                     System.err.println(
272                             "Failed to load signer \"" + signer.name + "\": "
273                                     + e.getMessage());
274                     System.exit(2);
275                     return;
276                 } catch (Exception e) {
277                     System.err.println("Failed to load signer \"" + signer.name + "\"");
278                     e.printStackTrace();
279                     System.exit(2);
280                     return;
281                 }
282                 String v1SigBasename;
283                 if (signer.v1SigFileBasename != null) {
284                     v1SigBasename = signer.v1SigFileBasename;
285                 } else if (signer.keystoreKeyAlias != null) {
286                     v1SigBasename = signer.keystoreKeyAlias;
287                 } else if (signer.keyFile != null) {
288                     String keyFileName = new File(signer.keyFile).getName();
289                     int delimiterIndex = keyFileName.indexOf('.');
290                     if (delimiterIndex == -1) {
291                         v1SigBasename = keyFileName;
292                     } else {
293                         v1SigBasename = keyFileName.substring(0, delimiterIndex);
294                     }
295                 } else {
296                     throw new RuntimeException(
297                             "Neither KeyStore key alias nor private key file available");
298                 }
299                 ApkSigner.SignerConfig signerConfig =
300                         new ApkSigner.SignerConfig.Builder(
301                                 v1SigBasename, signer.privateKey, signer.certs)
302                         .build();
303                 signerConfigs.add(signerConfig);
304             }
305         }
306 
307         if (outputApk == null) {
308             outputApk = inputApk;
309         }
310         File tmpOutputApk;
311         if (inputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) {
312             tmpOutputApk = File.createTempFile("apksigner", ".apk");
313             tmpOutputApk.deleteOnExit();
314         } else {
315             tmpOutputApk = outputApk;
316         }
317         ApkSigner.Builder apkSignerBuilder =
318                 new ApkSigner.Builder(signerConfigs)
319                         .setInputApk(inputApk)
320                         .setOutputApk(tmpOutputApk)
321                         .setOtherSignersSignaturesPreserved(false)
322                         .setV1SigningEnabled(v1SigningEnabled)
323                         .setV2SigningEnabled(v2SigningEnabled)
324                         .setDebuggableApkPermitted(debuggableApkPermitted);
325         if (minSdkVersionSpecified) {
326             apkSignerBuilder.setMinSdkVersion(minSdkVersion);
327         }
328         ApkSigner apkSigner = apkSignerBuilder.build();
329         try {
330             apkSigner.sign();
331         } catch (MinSdkVersionException e) {
332             String msg = e.getMessage();
333             if (!msg.endsWith(".")) {
334                 msg += '.';
335             }
336             throw new MinSdkVersionException(
337                     "Failed to determine APK's minimum supported platform version"
338                             + ". Use --min-sdk-version to override",
339                     e);
340         }
341         if (!tmpOutputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) {
342             Files.move(
343                     tmpOutputApk.toPath(), outputApk.toPath(), StandardCopyOption.REPLACE_EXISTING);
344         }
345 
346         if (verbose) {
347             System.out.println("Signed");
348         }
349     }
350 
verify(String[] params)351     private static void verify(String[] params) throws Exception {
352         if (params.length == 0) {
353             printUsage(HELP_PAGE_VERIFY);
354             return;
355         }
356 
357         File inputApk = null;
358         int minSdkVersion = 1;
359         boolean minSdkVersionSpecified = false;
360         int maxSdkVersion = Integer.MAX_VALUE;
361         boolean maxSdkVersionSpecified = false;
362         boolean printCerts = false;
363         boolean verbose = false;
364         boolean warningsTreatedAsErrors = false;
365         OptionsParser optionsParser = new OptionsParser(params);
366         String optionName;
367         String optionOriginalForm = null;
368         while ((optionName = optionsParser.nextOption()) != null) {
369             optionOriginalForm = optionsParser.getOptionOriginalForm();
370             if ("min-sdk-version".equals(optionName)) {
371                 minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level");
372                 minSdkVersionSpecified = true;
373             } else if ("max-sdk-version".equals(optionName)) {
374                 maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
375                 maxSdkVersionSpecified = true;
376             } else if ("print-certs".equals(optionName)) {
377                 printCerts = optionsParser.getOptionalBooleanValue(true);
378             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
379                 verbose = optionsParser.getOptionalBooleanValue(true);
380             } else if ("Werr".equals(optionName)) {
381                 warningsTreatedAsErrors = optionsParser.getOptionalBooleanValue(true);
382             } else if (("help".equals(optionName)) || ("h".equals(optionName))) {
383                 printUsage(HELP_PAGE_VERIFY);
384                 return;
385             } else if ("in".equals(optionName)) {
386                 inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
387             } else {
388                 throw new ParameterException(
389                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
390                                 + " options.");
391             }
392         }
393         params = optionsParser.getRemainingParams();
394 
395         if (inputApk != null) {
396             // Input APK has been specified in preceding parameters. We don't expect any more
397             // parameters.
398             if (params.length > 0) {
399                 throw new ParameterException(
400                         "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]);
401             }
402         } else {
403             // Input APK has not been specified in preceding parameters. The next parameter is
404             // supposed to be the input APK.
405             if (params.length < 1) {
406                 throw new ParameterException("Missing APK");
407             } else if (params.length > 1) {
408                 throw new ParameterException(
409                         "Unexpected parameter(s) after APK (" + params[1] + ")");
410             }
411             inputApk = new File(params[0]);
412         }
413 
414         if ((minSdkVersionSpecified) && (maxSdkVersionSpecified)
415                 && (minSdkVersion > maxSdkVersion)) {
416             throw new ParameterException(
417                     "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion
418                             + ")");
419         }
420 
421         ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk);
422         if (minSdkVersionSpecified) {
423             apkVerifierBuilder.setMinCheckedPlatformVersion(minSdkVersion);
424         }
425         if (maxSdkVersionSpecified) {
426             apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion);
427         }
428         ApkVerifier apkVerifier = apkVerifierBuilder.build();
429         ApkVerifier.Result result;
430         try {
431             result = apkVerifier.verify();
432         } catch (MinSdkVersionException e) {
433             String msg = e.getMessage();
434             if (!msg.endsWith(".")) {
435                 msg += '.';
436             }
437             throw new MinSdkVersionException(
438                     "Failed to determine APK's minimum supported platform version"
439                             + ". Use --min-sdk-version to override",
440                     e);
441         }
442         boolean verified = result.isVerified();
443 
444         boolean warningsEncountered = false;
445         if (verified) {
446             List<X509Certificate> signerCerts = result.getSignerCertificates();
447             if (verbose) {
448                 System.out.println("Verifies");
449                 System.out.println(
450                         "Verified using v1 scheme (JAR signing): "
451                                 + result.isVerifiedUsingV1Scheme());
452                 System.out.println(
453                         "Verified using v2 scheme (APK Signature Scheme v2): "
454                                 + result.isVerifiedUsingV2Scheme());
455                 System.out.println("Number of signers: " + signerCerts.size());
456             }
457             if (printCerts) {
458                 int signerNumber = 0;
459                 MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
460                 MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
461                 MessageDigest md5 = MessageDigest.getInstance("MD5");
462                 for (X509Certificate signerCert : signerCerts) {
463                     signerNumber++;
464                     System.out.println(
465                             "Signer #" + signerNumber + " certificate DN"
466                                     + ": " + signerCert.getSubjectDN());
467                     byte[] encodedCert = signerCert.getEncoded();
468                     System.out.println(
469                             "Signer #" + signerNumber + " certificate SHA-256 digest: "
470                                     + HexEncoding.encode(sha256.digest(encodedCert)));
471                     System.out.println(
472                             "Signer #" + signerNumber + " certificate SHA-1 digest: "
473                                     + HexEncoding.encode(sha1.digest(encodedCert)));
474                     System.out.println(
475                             "Signer #" + signerNumber + " certificate MD5 digest: "
476                                     + HexEncoding.encode(md5.digest(encodedCert)));
477                     if (verbose) {
478                         PublicKey publicKey = signerCert.getPublicKey();
479                         System.out.println(
480                                 "Signer #" + signerNumber + " key algorithm: "
481                                         + publicKey.getAlgorithm());
482                         int keySize = -1;
483                         if (publicKey instanceof RSAKey) {
484                             keySize = ((RSAKey) publicKey).getModulus().bitLength();
485                         } else if (publicKey instanceof ECKey) {
486                             keySize = ((ECKey) publicKey).getParams()
487                                     .getOrder().bitLength();
488                         } else if (publicKey instanceof DSAKey) {
489                             // DSA parameters may be inherited from the certificate. We
490                             // don't handle this case at the moment.
491                             DSAParams dsaParams = ((DSAKey) publicKey).getParams();
492                             if (dsaParams != null) {
493                                 keySize = dsaParams.getP().bitLength();
494                             }
495                         }
496                         System.out.println(
497                                 "Signer #" + signerNumber + " key size (bits): "
498                                         + ((keySize != -1)
499                                                 ? String.valueOf(keySize) : "n/a"));
500                         byte[] encodedKey = publicKey.getEncoded();
501                         System.out.println(
502                                 "Signer #" + signerNumber + " public key SHA-256 digest: "
503                                         + HexEncoding.encode(sha256.digest(encodedKey)));
504                         System.out.println(
505                                 "Signer #" + signerNumber + " public key SHA-1 digest: "
506                                         + HexEncoding.encode(sha1.digest(encodedKey)));
507                         System.out.println(
508                                 "Signer #" + signerNumber + " public key MD5 digest: "
509                                         + HexEncoding.encode(md5.digest(encodedKey)));
510                     }
511                 }
512             }
513         } else {
514             System.err.println("DOES NOT VERIFY");
515         }
516 
517         for (ApkVerifier.IssueWithParams error : result.getErrors()) {
518             System.err.println("ERROR: " + error);
519         }
520 
521         @SuppressWarnings("resource") // false positive -- this resource is not opened here
522         PrintStream warningsOut = (warningsTreatedAsErrors) ? System.err : System.out;
523         for (ApkVerifier.IssueWithParams warning : result.getWarnings()) {
524             warningsEncountered = true;
525             warningsOut.println("WARNING: " + warning);
526         }
527         for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
528             String signerName = signer.getName();
529             for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
530                 System.err.println("ERROR: JAR signer " + signerName + ": " + error);
531             }
532             for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
533                 warningsEncountered = true;
534                 warningsOut.println("WARNING: JAR signer " + signerName + ": " + warning);
535             }
536         }
537         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
538             String signerName = "signer #" + (signer.getIndex() + 1);
539             for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
540                 System.err.println(
541                         "ERROR: APK Signature Scheme v2 " + signerName + ": " + error);
542             }
543             for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
544                 warningsEncountered = true;
545                 warningsOut.println(
546                         "WARNING: APK Signature Scheme v2 " + signerName + ": " + warning);
547             }
548         }
549 
550         if (!verified) {
551             System.exit(1);
552             return;
553         }
554         if ((warningsTreatedAsErrors) && (warningsEncountered)) {
555             System.exit(1);
556             return;
557         }
558     }
559 
printUsage(String page)560     private static void printUsage(String page) {
561         try (BufferedReader in =
562                 new BufferedReader(
563                         new InputStreamReader(
564                                 ApkSignerTool.class.getResourceAsStream(page),
565                                 StandardCharsets.UTF_8))) {
566             String line;
567             while ((line = in.readLine()) != null) {
568                 System.out.println(line);
569             }
570         } catch (IOException e) {
571             throw new RuntimeException("Failed to read " + page + " resource");
572         }
573     }
574 
575     private static class ProviderInstallSpec {
576         String className;
577         String constructorParam;
578         Integer position;
579 
isEmpty()580         private boolean isEmpty() {
581             return (className == null) && (constructorParam == null) && (position == null);
582         }
583 
installProvider()584         private void installProvider() throws Exception {
585             if (className == null) {
586                 throw new ParameterException(
587                         "JCA Provider class name (--provider-class) must be specified");
588             }
589 
590             Class<?> providerClass = Class.forName(className);
591             if (!Provider.class.isAssignableFrom(providerClass)) {
592                 throw new ParameterException(
593                         "JCA Provider class " + providerClass + " not subclass of "
594                                 + Provider.class.getName());
595             }
596             Provider provider;
597             if (constructorParam != null) {
598                 // Single-arg Provider constructor
599                 provider =
600                         (Provider) providerClass.getConstructor(String.class)
601                                 .newInstance(constructorParam);
602             } else {
603                 // No-arg Provider constructor
604                 provider = (Provider) providerClass.getConstructor().newInstance();
605             }
606 
607             if (position == null) {
608                 Security.addProvider(provider);
609             } else {
610                 Security.insertProviderAt(provider, position);
611             }
612         }
613     }
614 
615     private static class SignerParams {
616         String name;
617 
618         String keystoreFile;
619         String keystoreKeyAlias;
620         String keystorePasswordSpec;
621         String keyPasswordSpec;
622         Charset passwordCharset;
623         String keystoreType;
624         String keystoreProviderName;
625         String keystoreProviderClass;
626         String keystoreProviderArg;
627 
628         String keyFile;
629         String certFile;
630 
631         String v1SigFileBasename;
632 
633         PrivateKey privateKey;
634         List<X509Certificate> certs;
635 
isEmpty()636         private boolean isEmpty() {
637             return (name == null)
638                     && (keystoreFile == null)
639                     && (keystoreKeyAlias == null)
640                     && (keystorePasswordSpec == null)
641                     && (keyPasswordSpec == null)
642                     && (passwordCharset == null)
643                     && (keystoreType == null)
644                     && (keystoreProviderName == null)
645                     && (keystoreProviderClass == null)
646                     && (keystoreProviderArg == null)
647                     && (keyFile == null)
648                     && (certFile == null)
649                     && (v1SigFileBasename == null)
650                     && (privateKey == null)
651                     && (certs == null);
652         }
653 
loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever)654         private void loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever) throws Exception {
655             if (keystoreFile != null) {
656                 if (keyFile != null) {
657                     throw new ParameterException(
658                             "--ks and --key may not be specified at the same time");
659                 } else if (certFile != null) {
660                     throw new ParameterException(
661                             "--ks and --cert may not be specified at the same time");
662                 }
663                 loadPrivateKeyAndCertsFromKeyStore(passwordRetriever);
664             } else if (keyFile != null) {
665                 loadPrivateKeyAndCertsFromFiles(passwordRetriever);
666             } else {
667                 throw new ParameterException(
668                         "KeyStore (--ks) or private key file (--key) must be specified");
669             }
670         }
671 
loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever)672         private void loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever)
673                 throws Exception {
674             if (keystoreFile == null) {
675                 throw new ParameterException("KeyStore (--ks) must be specified");
676             }
677 
678             // 1. Obtain a KeyStore implementation
679             String ksType = (keystoreType != null) ? keystoreType : KeyStore.getDefaultType();
680             KeyStore ks;
681             if (keystoreProviderName != null) {
682                 // Use a named Provider (assumes the provider is already installed)
683                 ks = KeyStore.getInstance(ksType, keystoreProviderName);
684             } else if (keystoreProviderClass != null) {
685                 // Use a new Provider instance (does not require the provider to be installed)
686                 Class<?> ksProviderClass = Class.forName(keystoreProviderClass);
687                 if (!Provider.class.isAssignableFrom(ksProviderClass)) {
688                     throw new ParameterException(
689                             "Keystore Provider class " + keystoreProviderClass + " not subclass of "
690                                     + Provider.class.getName());
691                 }
692                 Provider ksProvider;
693                 if (keystoreProviderArg != null) {
694                     // Single-arg Provider constructor
695                     ksProvider =
696                             (Provider) ksProviderClass.getConstructor(String.class)
697                                     .newInstance(keystoreProviderArg);
698                 } else {
699                     // No-arg Provider constructor
700                     ksProvider = (Provider) ksProviderClass.getConstructor().newInstance();
701                 }
702                 ks = KeyStore.getInstance(ksType, ksProvider);
703             } else {
704                 // Use the highest-priority Provider which offers the requested KeyStore type
705                 ks = KeyStore.getInstance(ksType);
706             }
707 
708             // 2. Load the KeyStore
709             List<char[]> keystorePasswords;
710             Charset[] additionalPasswordEncodings;
711             {
712                 String keystorePasswordSpec =
713                         (this.keystorePasswordSpec != null)
714                                 ?  this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN;
715                 additionalPasswordEncodings =
716                         (passwordCharset != null)
717                                 ? new Charset[] {passwordCharset} : new Charset[0];
718                 keystorePasswords =
719                         passwordRetriever.getPasswords(
720                                 keystorePasswordSpec,
721                                 "Keystore password for " + name,
722                                 additionalPasswordEncodings);
723                 loadKeyStoreFromFile(
724                         ks,
725                         "NONE".equals(keystoreFile) ? null : keystoreFile,
726                         keystorePasswords);
727             }
728 
729             // 3. Load the PrivateKey and cert chain from KeyStore
730             String keyAlias = null;
731             PrivateKey key = null;
732             try {
733                 if (keystoreKeyAlias == null) {
734                     // Private key entry alias not specified. Find the key entry contained in this
735                     // KeyStore. If the KeyStore contains multiple key entries, return an error.
736                     Enumeration<String> aliases = ks.aliases();
737                     if (aliases != null) {
738                         while (aliases.hasMoreElements()) {
739                             String entryAlias = aliases.nextElement();
740                             if (ks.isKeyEntry(entryAlias)) {
741                                 keyAlias = entryAlias;
742                                 if (keystoreKeyAlias != null) {
743                                     throw new ParameterException(
744                                             keystoreFile + " contains multiple key entries"
745                                             + ". --ks-key-alias option must be used to specify"
746                                             + " which entry to use.");
747                                 }
748                                 keystoreKeyAlias = keyAlias;
749                             }
750                         }
751                     }
752                     if (keystoreKeyAlias == null) {
753                         throw new ParameterException(
754                                 keystoreFile + " does not contain key entries");
755                     }
756                 }
757 
758                 // Private key entry alias known. Load that entry's private key.
759                 keyAlias = keystoreKeyAlias;
760                 if (!ks.isKeyEntry(keyAlias)) {
761                     throw new ParameterException(
762                             keystoreFile + " entry \"" + keyAlias + "\" does not contain a key");
763                 }
764 
765                 Key entryKey;
766                 if (keyPasswordSpec != null) {
767                     // Key password spec is explicitly specified. Use this spec to obtain the
768                     // password and then load the key using that password.
769                     List<char[]> keyPasswords =
770                             passwordRetriever.getPasswords(
771                                     keyPasswordSpec,
772                                     "Key \"" + keyAlias + "\" password for " + name,
773                                     additionalPasswordEncodings);
774                     entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
775                 } else {
776                     // Key password spec is not specified. This means we should assume that key
777                     // password is the same as the keystore password and that, if this assumption is
778                     // wrong, we should prompt for key password and retry loading the key using that
779                     // password.
780                     try {
781                         entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords);
782                     } catch (UnrecoverableKeyException expected) {
783                         List<char[]> keyPasswords =
784                                 passwordRetriever.getPasswords(
785                                         PasswordRetriever.SPEC_STDIN,
786                                         "Key \"" + keyAlias + "\" password for " + name,
787                                         additionalPasswordEncodings);
788                         entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
789                     }
790                 }
791 
792                 if (entryKey == null) {
793                     throw new ParameterException(
794                             keystoreFile + " entry \"" + keyAlias + "\" does not contain a key");
795                 } else if (!(entryKey instanceof PrivateKey)) {
796                     throw new ParameterException(
797                             keystoreFile + " entry \"" + keyAlias + "\" does not contain a private"
798                                     + " key. It contains a key of algorithm: "
799                                     + entryKey.getAlgorithm());
800                 }
801                 key = (PrivateKey) entryKey;
802             } catch (UnrecoverableKeyException e) {
803                 throw new IOException(
804                         "Failed to obtain key with alias \"" + keyAlias + "\" from " + keystoreFile
805                                 + ". Wrong password?",
806                         e);
807             }
808             this.privateKey = key;
809             Certificate[] certChain = ks.getCertificateChain(keyAlias);
810             if ((certChain == null) || (certChain.length == 0)) {
811                 throw new ParameterException(
812                         keystoreFile + " entry \"" + keyAlias + "\" does not contain certificates");
813             }
814             this.certs = new ArrayList<>(certChain.length);
815             for (Certificate cert : certChain) {
816                 this.certs.add((X509Certificate) cert);
817             }
818         }
819 
820         /**
821          * Loads the password-protected keystore from storage.
822          *
823          * @param file file backing the keystore or {@code null} if the keystore is not file-backed,
824          *        for example, a PKCS #11 KeyStore.
825          */
loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords)826         private static void loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords)
827                 throws Exception {
828             Exception lastFailure = null;
829             for (char[] password : passwords) {
830                 try {
831                     if (file != null) {
832                         try (FileInputStream in = new FileInputStream(file)) {
833                             ks.load(in, password);
834                         }
835                     } else {
836                         ks.load(null, password);
837                     }
838                     return;
839                 } catch (Exception e) {
840                     lastFailure = e;
841                 }
842             }
843             if (lastFailure == null) {
844                 throw new RuntimeException("No keystore passwords");
845             } else {
846                 throw lastFailure;
847             }
848         }
849 
getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords)850         private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords)
851                 throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
852             UnrecoverableKeyException lastFailure = null;
853             for (char[] password : passwords) {
854                 try {
855                     return ks.getKey(keyAlias, password);
856                 } catch (UnrecoverableKeyException e) {
857                     lastFailure = e;
858                 }
859             }
860             if (lastFailure == null) {
861                 throw new RuntimeException("No key passwords");
862             } else {
863                 throw lastFailure;
864             }
865         }
866 
loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriver)867         private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriver)
868                 throws Exception {
869             if (keyFile == null) {
870                 throw new ParameterException("Private key file (--key) must be specified");
871             }
872             if (certFile == null) {
873                 throw new ParameterException("Certificate file (--cert) must be specified");
874             }
875             byte[] privateKeyBlob = readFully(new File(keyFile));
876 
877             PKCS8EncodedKeySpec keySpec;
878             // Potentially encrypted key blob
879             try {
880                 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo =
881                         new EncryptedPrivateKeyInfo(privateKeyBlob);
882 
883                 // The blob is indeed an encrypted private key blob
884                 String passwordSpec =
885                         (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN;
886                 Charset[] additionalPasswordEncodings =
887                         (passwordCharset != null)
888                                 ? new Charset[] {passwordCharset} : new Charset[0];
889                 List<char[]> keyPasswords =
890                         passwordRetriver.getPasswords(
891                                 passwordSpec,
892                                 "Private key password for " + name,
893                                 additionalPasswordEncodings);
894                 keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords);
895             } catch (IOException e) {
896                 // The blob is not an encrypted private key blob
897                 if (keyPasswordSpec == null) {
898                     // Given that no password was specified, assume the blob is an unencrypted
899                     // private key blob
900                     keySpec = new PKCS8EncodedKeySpec(privateKeyBlob);
901                 } else {
902                     throw new InvalidKeySpecException(
903                             "Failed to parse encrypted private key blob " + keyFile, e);
904                 }
905             }
906 
907             // Load the private key from its PKCS #8 encoded form.
908             try {
909                 privateKey = loadPkcs8EncodedPrivateKey(keySpec);
910             } catch (InvalidKeySpecException e) {
911                 throw new InvalidKeySpecException(
912                         "Failed to load PKCS #8 encoded private key from " + keyFile, e);
913             }
914 
915             // Load certificates
916             Collection<? extends Certificate> certs;
917             try (FileInputStream in = new FileInputStream(certFile)) {
918                 certs = CertificateFactory.getInstance("X.509").generateCertificates(in);
919             }
920             List<X509Certificate> certList = new ArrayList<>(certs.size());
921             for (Certificate cert : certs) {
922                 certList.add((X509Certificate) cert);
923             }
924             this.certs = certList;
925         }
926 
decryptPkcs8EncodedKey( EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords)927         private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey(
928                 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords)
929                 throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
930             SecretKeyFactory keyFactory =
931                     SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
932             InvalidKeySpecException lastKeySpecException = null;
933             InvalidKeyException lastKeyException = null;
934             for (char[] password : passwords) {
935                 PBEKeySpec decryptionKeySpec = new PBEKeySpec(password);
936                 try {
937                     SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec);
938                     return encryptedPrivateKeyInfo.getKeySpec(decryptionKey);
939                 } catch (InvalidKeySpecException e) {
940                     lastKeySpecException = e;
941                 } catch (InvalidKeyException e) {
942                     lastKeyException = e;
943                 }
944             }
945             if ((lastKeyException == null) && (lastKeySpecException == null)) {
946                 throw new RuntimeException("No passwords");
947             } else if (lastKeyException != null) {
948                 throw lastKeyException;
949             } else {
950                 throw lastKeySpecException;
951             }
952         }
953 
loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec)954         private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec)
955                 throws InvalidKeySpecException, NoSuchAlgorithmException {
956             try {
957                 return KeyFactory.getInstance("RSA").generatePrivate(spec);
958             } catch (InvalidKeySpecException expected) {
959             }
960             try {
961                 return KeyFactory.getInstance("EC").generatePrivate(spec);
962             } catch (InvalidKeySpecException expected) {
963             }
964             try {
965                 return KeyFactory.getInstance("DSA").generatePrivate(spec);
966             } catch (InvalidKeySpecException expected) {
967             }
968             throw new InvalidKeySpecException("Not an RSA, EC, or DSA private key");
969         }
970     }
971 
readFully(File file)972     private static byte[] readFully(File file) throws IOException {
973         ByteArrayOutputStream result = new ByteArrayOutputStream();
974         try (FileInputStream in = new FileInputStream(file)) {
975             drain(in, result);
976         }
977         return result.toByteArray();
978     }
979 
drain(InputStream in, OutputStream out)980     private static void drain(InputStream in, OutputStream out) throws IOException {
981         byte[] buf = new byte[65536];
982         int chunkSize;
983         while ((chunkSize = in.read(buf)) != -1) {
984             out.write(buf, 0, chunkSize);
985         }
986     }
987 
988     /**
989      * Indicates that there is an issue with command-line parameters provided to this tool.
990      */
991     private static class ParameterException extends Exception {
992         private static final long serialVersionUID = 1L;
993 
ParameterException(String message)994         ParameterException(String message) {
995             super(message);
996         }
997     }
998 }
999