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