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