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