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