| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.apksigner; |
| |
| import com.android.apksig.ApkSigner; |
| import com.android.apksig.ApkVerifier; |
| import com.android.apksig.SigningCertificateLineage; |
| import com.android.apksig.SigningCertificateLineage.SignerCapabilities; |
| import com.android.apksig.apk.ApkFormatException; |
| import com.android.apksig.apk.MinSdkVersionException; |
| import com.android.apksig.util.DataSource; |
| import com.android.apksig.util.DataSources; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.PrintStream; |
| import java.io.RandomAccessFile; |
| import java.nio.ByteOrder; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.StandardCopyOption; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.Provider; |
| import java.security.PublicKey; |
| import java.security.Security; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.X509Certificate; |
| import java.security.interfaces.DSAKey; |
| import java.security.interfaces.DSAParams; |
| import java.security.interfaces.ECKey; |
| import java.security.interfaces.RSAKey; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Command-line tool for signing APKs and for checking whether an APK's signature are expected to |
| * verify on Android devices. |
| */ |
| public class ApkSignerTool { |
| |
| private static final String VERSION = "0.9"; |
| private static final String HELP_PAGE_GENERAL = "help.txt"; |
| private static final String HELP_PAGE_SIGN = "help_sign.txt"; |
| private static final String HELP_PAGE_VERIFY = "help_verify.txt"; |
| private static final String HELP_PAGE_ROTATE = "help_rotate.txt"; |
| private static final String HELP_PAGE_LINEAGE = "help_lineage.txt"; |
| |
| private static MessageDigest sha256 = null; |
| private static MessageDigest sha1 = null; |
| private static MessageDigest md5 = null; |
| |
| public static final int ZIP_MAGIC = 0x04034b50; |
| |
| public static void main(String[] params) throws Exception { |
| if ((params.length == 0) || ("--help".equals(params[0])) || ("-h".equals(params[0]))) { |
| printUsage(HELP_PAGE_GENERAL); |
| return; |
| } else if ("--version".equals(params[0])) { |
| System.out.println(VERSION); |
| return; |
| } |
| |
| // BEGIN-AOSP |
| addProviders(); |
| // END-AOSP |
| |
| String cmd = params[0]; |
| try { |
| if ("sign".equals(cmd)) { |
| sign(Arrays.copyOfRange(params, 1, params.length)); |
| return; |
| } else if ("verify".equals(cmd)) { |
| verify(Arrays.copyOfRange(params, 1, params.length)); |
| return; |
| } else if ("rotate".equals(cmd)) { |
| rotate(Arrays.copyOfRange(params, 1, params.length)); |
| return; |
| } else if ("lineage".equals(cmd)) { |
| lineage(Arrays.copyOfRange(params, 1, params.length)); |
| return; |
| } else if ("help".equals(cmd)) { |
| printUsage(HELP_PAGE_GENERAL); |
| return; |
| } else if ("version".equals(cmd)) { |
| System.out.println(VERSION); |
| return; |
| } else { |
| throw new ParameterException( |
| "Unsupported command: " + cmd + ". See --help for supported commands"); |
| } |
| } catch (ParameterException | OptionsParser.OptionsException e) { |
| System.err.println(e.getMessage()); |
| System.exit(1); |
| return; |
| } |
| } |
| |
| // BEGIN-AOSP |
| /** |
| * Adds additional security providers to add support for signature algorithms not covered by |
| * the default providers. |
| */ |
| private static void addProviders() { |
| try { |
| Security.addProvider(new org.conscrypt.OpenSSLProvider()); |
| } catch (UnsatisfiedLinkError e) { |
| // This is expected if the library path does not include the native conscrypt library; |
| // the default providers support all but PSS algorithms. |
| } |
| } |
| // END-AOSP |
| |
| private static void sign(String[] params) throws Exception { |
| if (params.length == 0) { |
| printUsage(HELP_PAGE_SIGN); |
| return; |
| } |
| |
| File outputApk = null; |
| File inputApk = null; |
| boolean verbose = false; |
| boolean v1SigningEnabled = true; |
| boolean v2SigningEnabled = true; |
| boolean v3SigningEnabled = true; |
| boolean v4SigningEnabled = true; |
| boolean forceSourceStampOverwrite = false; |
| boolean verityEnabled = false; |
| boolean debuggableApkPermitted = true; |
| int minSdkVersion = 1; |
| boolean minSdkVersionSpecified = false; |
| int maxSdkVersion = Integer.MAX_VALUE; |
| List<SignerParams> signers = new ArrayList<>(1); |
| SignerParams signerParams = new SignerParams(); |
| SigningCertificateLineage lineage = null; |
| SignerParams sourceStampSignerParams = new SignerParams(); |
| SigningCertificateLineage sourceStampLineage = null; |
| List<ProviderInstallSpec> providers = new ArrayList<>(); |
| ProviderInstallSpec providerParams = new ProviderInstallSpec(); |
| OptionsParser optionsParser = new OptionsParser(params); |
| String optionName; |
| String optionOriginalForm = null; |
| boolean v4SigningFlagFound = false; |
| boolean sourceStampFlagFound = false; |
| boolean deterministicDsaSigning = false; |
| boolean otherSignersSignaturesPreserved = false; |
| while ((optionName = optionsParser.nextOption()) != null) { |
| optionOriginalForm = optionsParser.getOptionOriginalForm(); |
| if (("help".equals(optionName)) || ("h".equals(optionName))) { |
| printUsage(HELP_PAGE_SIGN); |
| return; |
| } else if ("out".equals(optionName)) { |
| outputApk = new File(optionsParser.getRequiredValue("Output file name")); |
| } else if ("in".equals(optionName)) { |
| inputApk = new File(optionsParser.getRequiredValue("Input file name")); |
| } else if ("min-sdk-version".equals(optionName)) { |
| minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level"); |
| minSdkVersionSpecified = true; |
| } else if ("max-sdk-version".equals(optionName)) { |
| maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level"); |
| } else if ("v1-signing-enabled".equals(optionName)) { |
| v1SigningEnabled = optionsParser.getOptionalBooleanValue(true); |
| } else if ("v2-signing-enabled".equals(optionName)) { |
| v2SigningEnabled = optionsParser.getOptionalBooleanValue(true); |
| } else if ("v3-signing-enabled".equals(optionName)) { |
| v3SigningEnabled = optionsParser.getOptionalBooleanValue(true); |
| } else if ("v4-signing-enabled".equals(optionName)) { |
| v4SigningEnabled = optionsParser.getOptionalBooleanValue(true); |
| v4SigningFlagFound = true; |
| } else if ("force-stamp-overwrite".equals(optionName)) { |
| forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true); |
| } else if ("verity-enabled".equals(optionName)) { |
| verityEnabled = optionsParser.getOptionalBooleanValue(true); |
| } else if ("debuggable-apk-permitted".equals(optionName)) { |
| debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true); |
| } else if ("next-signer".equals(optionName)) { |
| if (!signerParams.isEmpty()) { |
| signers.add(signerParams); |
| signerParams = new SignerParams(); |
| } |
| } else if ("ks".equals(optionName)) { |
| signerParams.setKeystoreFile(optionsParser.getRequiredValue("KeyStore file")); |
| } else if ("ks-key-alias".equals(optionName)) { |
| signerParams.setKeystoreKeyAlias( |
| optionsParser.getRequiredValue("KeyStore key alias")); |
| } else if ("ks-pass".equals(optionName)) { |
| signerParams.setKeystorePasswordSpec( |
| optionsParser.getRequiredValue("KeyStore password")); |
| } else if ("key-pass".equals(optionName)) { |
| signerParams.setKeyPasswordSpec(optionsParser.getRequiredValue("Key password")); |
| } else if ("pass-encoding".equals(optionName)) { |
| String charsetName = |
| optionsParser.getRequiredValue("Password character encoding"); |
| try { |
| signerParams.setPasswordCharset( |
| PasswordRetriever.getCharsetByName(charsetName)); |
| } catch (IllegalArgumentException e) { |
| throw new ParameterException( |
| "Unsupported password character encoding requested using" |
| + " --pass-encoding: " + charsetName); |
| } |
| } else if ("v1-signer-name".equals(optionName)) { |
| signerParams.setV1SigFileBasename( |
| optionsParser.getRequiredValue("JAR signature file basename")); |
| } else if ("ks-type".equals(optionName)) { |
| signerParams.setKeystoreType(optionsParser.getRequiredValue("KeyStore type")); |
| } else if ("ks-provider-name".equals(optionName)) { |
| signerParams.setKeystoreProviderName( |
| optionsParser.getRequiredValue("JCA KeyStore Provider name")); |
| } else if ("ks-provider-class".equals(optionName)) { |
| signerParams.setKeystoreProviderClass( |
| optionsParser.getRequiredValue("JCA KeyStore Provider class name")); |
| } else if ("ks-provider-arg".equals(optionName)) { |
| signerParams.setKeystoreProviderArg( |
| optionsParser.getRequiredValue( |
| "JCA KeyStore Provider constructor argument")); |
| } else if ("key".equals(optionName)) { |
| signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file")); |
| } else if ("cert".equals(optionName)) { |
| signerParams.setCertFile(optionsParser.getRequiredValue("Certificate file")); |
| } else if ("lineage".equals(optionName)) { |
| File lineageFile = new File(optionsParser.getRequiredValue("Lineage File")); |
| lineage = getLineageFromInputFile(lineageFile); |
| } else if ("v".equals(optionName) || "verbose".equals(optionName)) { |
| verbose = optionsParser.getOptionalBooleanValue(true); |
| } else if ("next-provider".equals(optionName)) { |
| if (!providerParams.isEmpty()) { |
| providers.add(providerParams); |
| providerParams = new ProviderInstallSpec(); |
| } |
| } else if ("provider-class".equals(optionName)) { |
| providerParams.className = |
| optionsParser.getRequiredValue("JCA Provider class name"); |
| } else if ("provider-arg".equals(optionName)) { |
| providerParams.constructorParam = |
| optionsParser.getRequiredValue("JCA Provider constructor argument"); |
| } else if ("provider-pos".equals(optionName)) { |
| providerParams.position = |
| optionsParser.getRequiredIntValue("JCA Provider position"); |
| } else if ("stamp-signer".equals(optionName)) { |
| sourceStampFlagFound = true; |
| sourceStampSignerParams = processSignerParams(optionsParser); |
| } else if ("stamp-lineage".equals(optionName)) { |
| File stampLineageFile = new File( |
| optionsParser.getRequiredValue("Stamp Lineage File")); |
| sourceStampLineage = getLineageFromInputFile(stampLineageFile); |
| } else if ("deterministic-dsa-signing".equals(optionName)) { |
| deterministicDsaSigning = optionsParser.getOptionalBooleanValue(false); |
| } else if ("append-signature".equals(optionName)) { |
| otherSignersSignaturesPreserved = optionsParser.getOptionalBooleanValue(true); |
| } else { |
| throw new ParameterException( |
| "Unsupported option: " + optionOriginalForm + ". See --help for supported" |
| + " options."); |
| } |
| } |
| if (!signerParams.isEmpty()) { |
| signers.add(signerParams); |
| } |
| signerParams = null; |
| if (!providerParams.isEmpty()) { |
| providers.add(providerParams); |
| } |
| providerParams = null; |
| |
| if (signers.isEmpty()) { |
| throw new ParameterException("At least one signer must be specified"); |
| } |
| |
| params = optionsParser.getRemainingParams(); |
| if (inputApk != null) { |
| // Input APK has been specified via preceding parameters. We don't expect any more |
| // parameters. |
| if (params.length > 0) { |
| throw new ParameterException( |
| "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]); |
| } |
| } else { |
| // Input APK has not been specified via preceding parameters. The next parameter is |
| // supposed to be the path to input APK. |
| if (params.length < 1) { |
| throw new ParameterException("Missing input APK"); |
| } else if (params.length > 1) { |
| throw new ParameterException( |
| "Unexpected parameter(s) after input APK (" + params[1] + ")"); |
| } |
| inputApk = new File(params[0]); |
| } |
| if ((minSdkVersionSpecified) && (minSdkVersion > maxSdkVersion)) { |
| throw new ParameterException( |
| "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion |
| + ")"); |
| } |
| |
| // Install additional JCA Providers |
| for (ProviderInstallSpec providerInstallSpec : providers) { |
| providerInstallSpec.installProvider(); |
| } |
| |
| ApkSigner.SignerConfig sourceStampSignerConfig = null; |
| List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size()); |
| int signerNumber = 0; |
| try (PasswordRetriever passwordRetriever = new PasswordRetriever()) { |
| for (SignerParams signer : signers) { |
| signerNumber++; |
| signer.setName("signer #" + signerNumber); |
| ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever, |
| deterministicDsaSigning); |
| if (signerConfig == null) { |
| return; |
| } |
| signerConfigs.add(signerConfig); |
| } |
| if (sourceStampFlagFound) { |
| sourceStampSignerParams.setName("stamp signer"); |
| sourceStampSignerConfig = |
| getSignerConfig(sourceStampSignerParams, passwordRetriever, |
| deterministicDsaSigning); |
| if (sourceStampSignerConfig == null) { |
| return; |
| } |
| } |
| } |
| |
| if (outputApk == null) { |
| outputApk = inputApk; |
| } |
| File tmpOutputApk; |
| if (inputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) { |
| tmpOutputApk = File.createTempFile("apksigner", ".apk"); |
| tmpOutputApk.deleteOnExit(); |
| } else { |
| tmpOutputApk = outputApk; |
| } |
| ApkSigner.Builder apkSignerBuilder = |
| new ApkSigner.Builder(signerConfigs) |
| .setInputApk(inputApk) |
| .setOutputApk(tmpOutputApk) |
| .setOtherSignersSignaturesPreserved(otherSignersSignaturesPreserved) |
| .setV1SigningEnabled(v1SigningEnabled) |
| .setV2SigningEnabled(v2SigningEnabled) |
| .setV3SigningEnabled(v3SigningEnabled) |
| .setV4SigningEnabled(v4SigningEnabled) |
| .setForceSourceStampOverwrite(forceSourceStampOverwrite) |
| .setVerityEnabled(verityEnabled) |
| .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound) |
| .setDebuggableApkPermitted(debuggableApkPermitted) |
| .setSigningCertificateLineage(lineage); |
| if (minSdkVersionSpecified) { |
| apkSignerBuilder.setMinSdkVersion(minSdkVersion); |
| } |
| if (v4SigningEnabled) { |
| final File outputV4SignatureFile = |
| new File(outputApk.getCanonicalPath() + ".idsig"); |
| Files.deleteIfExists(outputV4SignatureFile.toPath()); |
| apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile); |
| } |
| if (sourceStampSignerConfig != null) { |
| apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig) |
| .setSourceStampSigningCertificateLineage(sourceStampLineage); |
| } |
| ApkSigner apkSigner = apkSignerBuilder.build(); |
| try { |
| apkSigner.sign(); |
| } catch (MinSdkVersionException e) { |
| String msg = e.getMessage(); |
| if (!msg.endsWith(".")) { |
| msg += '.'; |
| } |
| throw new MinSdkVersionException( |
| "Failed to determine APK's minimum supported platform version" |
| + ". Use --min-sdk-version to override", |
| e); |
| } |
| if (!tmpOutputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) { |
| Files.move( |
| tmpOutputApk.toPath(), outputApk.toPath(), StandardCopyOption.REPLACE_EXISTING); |
| } |
| |
| if (verbose) { |
| System.out.println("Signed"); |
| } |
| } |
| |
| private static ApkSigner.SignerConfig getSignerConfig(SignerParams signer, |
| PasswordRetriever passwordRetriever, boolean deterministicDsaSigning) { |
| try { |
| signer.loadPrivateKeyAndCerts(passwordRetriever); |
| } catch (ParameterException e) { |
| System.err.println( |
| "Failed to load signer \"" + signer.getName() + "\": " + e.getMessage()); |
| System.exit(2); |
| return null; |
| } catch (Exception e) { |
| System.err.println("Failed to load signer \"" + signer.getName() + "\""); |
| e.printStackTrace(); |
| System.exit(2); |
| return null; |
| } |
| String v1SigBasename; |
| if (signer.getV1SigFileBasename() != null) { |
| v1SigBasename = signer.getV1SigFileBasename(); |
| } else if (signer.getKeystoreKeyAlias() != null) { |
| v1SigBasename = signer.getKeystoreKeyAlias(); |
| } else if (signer.getKeyFile() != null) { |
| String keyFileName = new File(signer.getKeyFile()).getName(); |
| int delimiterIndex = keyFileName.indexOf('.'); |
| if (delimiterIndex == -1) { |
| v1SigBasename = keyFileName; |
| } else { |
| v1SigBasename = keyFileName.substring(0, delimiterIndex); |
| } |
| } else { |
| throw new RuntimeException("Neither KeyStore key alias nor private key file available"); |
| } |
| ApkSigner.SignerConfig signerConfig = |
| new ApkSigner.SignerConfig.Builder( |
| v1SigBasename, signer.getPrivateKey(), signer.getCerts(), |
| deterministicDsaSigning) |
| .build(); |
| return signerConfig; |
| } |
| |
| private static void verify(String[] params) throws Exception { |
| if (params.length == 0) { |
| printUsage(HELP_PAGE_VERIFY); |
| return; |
| } |
| |
| File inputApk = null; |
| int minSdkVersion = 1; |
| boolean minSdkVersionSpecified = false; |
| int maxSdkVersion = Integer.MAX_VALUE; |
| boolean maxSdkVersionSpecified = false; |
| boolean printCerts = false; |
| boolean verbose = false; |
| boolean warningsTreatedAsErrors = false; |
| boolean verifySourceStamp = false; |
| File v4SignatureFile = null; |
| OptionsParser optionsParser = new OptionsParser(params); |
| String optionName; |
| String optionOriginalForm = null; |
| String sourceCertDigest = null; |
| while ((optionName = optionsParser.nextOption()) != null) { |
| optionOriginalForm = optionsParser.getOptionOriginalForm(); |
| if ("min-sdk-version".equals(optionName)) { |
| minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level"); |
| minSdkVersionSpecified = true; |
| } else if ("max-sdk-version".equals(optionName)) { |
| maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level"); |
| maxSdkVersionSpecified = true; |
| } else if ("print-certs".equals(optionName)) { |
| printCerts = optionsParser.getOptionalBooleanValue(true); |
| } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { |
| verbose = optionsParser.getOptionalBooleanValue(true); |
| } else if ("Werr".equals(optionName)) { |
| warningsTreatedAsErrors = optionsParser.getOptionalBooleanValue(true); |
| } else if (("help".equals(optionName)) || ("h".equals(optionName))) { |
| printUsage(HELP_PAGE_VERIFY); |
| return; |
| } else if ("v4-signature-file".equals(optionName)) { |
| v4SignatureFile = new File(optionsParser.getRequiredValue( |
| "Input V4 Signature File")); |
| } else if ("in".equals(optionName)) { |
| inputApk = new File(optionsParser.getRequiredValue("Input APK file")); |
| } else if ("verify-source-stamp".equals(optionName)) { |
| verifySourceStamp = optionsParser.getOptionalBooleanValue(true); |
| } else if ("stamp-cert-digest".equals(optionName)) { |
| sourceCertDigest = optionsParser.getRequiredValue( |
| "Expected source stamp certificate digest"); |
| } else { |
| throw new ParameterException( |
| "Unsupported option: " + optionOriginalForm + ". See --help for supported" |
| + " options."); |
| } |
| } |
| params = optionsParser.getRemainingParams(); |
| |
| if (inputApk != null) { |
| // Input APK has been specified in preceding parameters. We don't expect any more |
| // parameters. |
| if (params.length > 0) { |
| throw new ParameterException( |
| "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]); |
| } |
| } else { |
| // Input APK has not been specified in preceding parameters. The next parameter is |
| // supposed to be the input APK. |
| if (params.length < 1) { |
| throw new ParameterException("Missing APK"); |
| } else if (params.length > 1) { |
| throw new ParameterException( |
| "Unexpected parameter(s) after APK (" + params[1] + ")"); |
| } |
| inputApk = new File(params[0]); |
| } |
| |
| if ((minSdkVersionSpecified) && (maxSdkVersionSpecified) |
| && (minSdkVersion > maxSdkVersion)) { |
| throw new ParameterException( |
| "Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion |
| + ")"); |
| } |
| |
| ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk); |
| if (minSdkVersionSpecified) { |
| apkVerifierBuilder.setMinCheckedPlatformVersion(minSdkVersion); |
| } |
| if (maxSdkVersionSpecified) { |
| apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion); |
| } |
| if (v4SignatureFile != null) { |
| if (!v4SignatureFile.exists()) { |
| throw new ParameterException("V4 signature file does not exist: " |
| + v4SignatureFile.getCanonicalPath()); |
| } |
| apkVerifierBuilder.setV4SignatureFile(v4SignatureFile); |
| } |
| |
| ApkVerifier apkVerifier = apkVerifierBuilder.build(); |
| ApkVerifier.Result result; |
| try { |
| result = verifySourceStamp |
| ? apkVerifier.verifySourceStamp(sourceCertDigest) |
| : apkVerifier.verify(); |
| } catch (MinSdkVersionException e) { |
| String msg = e.getMessage(); |
| if (!msg.endsWith(".")) { |
| msg += '.'; |
| } |
| throw new MinSdkVersionException( |
| "Failed to determine APK's minimum supported platform version" |
| + ". Use --min-sdk-version to override", |
| e); |
| } |
| |
| boolean verified = result.isVerified(); |
| ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo(); |
| boolean warningsEncountered = false; |
| if (verified) { |
| List<X509Certificate> signerCerts = result.getSignerCertificates(); |
| if (verbose) { |
| System.out.println("Verifies"); |
| System.out.println( |
| "Verified using v1 scheme (JAR signing): " |
| + result.isVerifiedUsingV1Scheme()); |
| System.out.println( |
| "Verified using v2 scheme (APK Signature Scheme v2): " |
| + result.isVerifiedUsingV2Scheme()); |
| System.out.println( |
| "Verified using v3 scheme (APK Signature Scheme v3): " |
| + result.isVerifiedUsingV3Scheme()); |
| System.out.println( |
| "Verified using v4 scheme (APK Signature Scheme v4): " |
| + result.isVerifiedUsingV4Scheme()); |
| System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified()); |
| if (!verifySourceStamp) { |
| System.out.println("Number of signers: " + signerCerts.size()); |
| } |
| } |
| if (printCerts) { |
| int signerNumber = 0; |
| for (X509Certificate signerCert : signerCerts) { |
| signerNumber++; |
| printCertificate(signerCert, "Signer #" + signerNumber, verbose); |
| } |
| if (sourceStampInfo != null) { |
| printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer", |
| verbose); |
| } |
| } |
| } else { |
| System.err.println("DOES NOT VERIFY"); |
| } |
| |
| for (ApkVerifier.IssueWithParams error : result.getErrors()) { |
| System.err.println("ERROR: " + error); |
| } |
| |
| @SuppressWarnings("resource") // false positive -- this resource is not opened here |
| PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out; |
| for (ApkVerifier.IssueWithParams warning : result.getWarnings()) { |
| warningsEncountered = true; |
| warningsOut.println("WARNING: " + warning); |
| } |
| for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { |
| String signerName = signer.getName(); |
| for (ApkVerifier.IssueWithParams error : signer.getErrors()) { |
| System.err.println("ERROR: JAR signer " + signerName + ": " + error); |
| } |
| for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { |
| warningsEncountered = true; |
| warningsOut.println("WARNING: JAR signer " + signerName + ": " + warning); |
| } |
| } |
| for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { |
| String signerName = "signer #" + (signer.getIndex() + 1); |
| for (ApkVerifier.IssueWithParams error : signer.getErrors()) { |
| System.err.println( |
| "ERROR: APK Signature Scheme v2 " + signerName + ": " + error); |
| } |
| for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { |
| warningsEncountered = true; |
| warningsOut.println( |
| "WARNING: APK Signature Scheme v2 " + signerName + ": " + warning); |
| } |
| } |
| for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) { |
| String signerName = "signer #" + (signer.getIndex() + 1); |
| for (ApkVerifier.IssueWithParams error : signer.getErrors()) { |
| System.err.println( |
| "ERROR: APK Signature Scheme v3 " + signerName + ": " + error); |
| } |
| for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { |
| warningsEncountered = true; |
| warningsOut.println( |
| "WARNING: APK Signature Scheme v3 " + signerName + ": " + warning); |
| } |
| } |
| |
| if (sourceStampInfo != null) { |
| for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) { |
| System.err.println("ERROR: SourceStamp: " + error); |
| } |
| for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) { |
| warningsOut.println("WARNING: SourceStamp: " + warning); |
| } |
| } |
| |
| if (!verified) { |
| System.exit(1); |
| return; |
| } |
| if ((warningsTreatedAsErrors) && (warningsEncountered)) { |
| System.exit(1); |
| return; |
| } |
| } |
| |
| private static void rotate(String[] params) throws Exception { |
| if (params.length == 0) { |
| printUsage(HELP_PAGE_ROTATE); |
| return; |
| } |
| |
| File outputKeyLineage = null; |
| File inputKeyLineage = null; |
| boolean verbose = false; |
| SignerParams oldSignerParams = null; |
| SignerParams newSignerParams = null; |
| int minSdkVersion = 0; |
| List<ProviderInstallSpec> providers = new ArrayList<>(); |
| ProviderInstallSpec providerParams = new ProviderInstallSpec(); |
| OptionsParser optionsParser = new OptionsParser(params); |
| String optionName; |
| String optionOriginalForm = null; |
| while ((optionName = optionsParser.nextOption()) != null) { |
| optionOriginalForm = optionsParser.getOptionOriginalForm(); |
| if (("help".equals(optionName)) || ("h".equals(optionName))) { |
| printUsage(HELP_PAGE_ROTATE); |
| return; |
| } else if ("out".equals(optionName)) { |
| outputKeyLineage = new File(optionsParser.getRequiredValue("Output file name")); |
| } else if ("in".equals(optionName)) { |
| inputKeyLineage = new File(optionsParser.getRequiredValue("Input file name")); |
| } else if ("old-signer".equals(optionName)) { |
| oldSignerParams = processSignerParams(optionsParser); |
| } else if ("new-signer".equals(optionName)) { |
| newSignerParams = processSignerParams(optionsParser); |
| } else if ("min-sdk-version".equals(optionName)) { |
| minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level"); |
| } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { |
| verbose = optionsParser.getOptionalBooleanValue(true); |
| } else if ("next-provider".equals(optionName)) { |
| if (!providerParams.isEmpty()) { |
| providers.add(providerParams); |
| providerParams = new ProviderInstallSpec(); |
| } |
| } else if ("provider-class".equals(optionName)) { |
| providerParams.className = |
| optionsParser.getRequiredValue("JCA Provider class name"); |
| } else if ("provider-arg".equals(optionName)) { |
| providerParams.constructorParam = |
| optionsParser.getRequiredValue("JCA Provider constructor argument"); |
| } else if ("provider-pos".equals(optionName)) { |
| providerParams.position = |
| optionsParser.getRequiredIntValue("JCA Provider position"); |
| } else { |
| throw new ParameterException( |
| "Unsupported option: " + optionOriginalForm + ". See --help for supported" |
| + " options."); |
| } |
| } |
| if (!providerParams.isEmpty()) { |
| providers.add(providerParams); |
| } |
| providerParams = null; |
| |
| if (oldSignerParams.isEmpty()) { |
| throw new ParameterException("Signer parameters for old signer not present"); |
| } |
| |
| if (newSignerParams.isEmpty()) { |
| throw new ParameterException("Signer parameters for new signer not present"); |
| } |
| |
| if (outputKeyLineage == null) { |
| throw new ParameterException("Output lineage file parameter not present"); |
| } |
| |
| params = optionsParser.getRemainingParams(); |
| if (params.length > 0) { |
| throw new ParameterException( |
| "Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]); |
| } |
| |
| |
| // Install additional JCA Providers |
| for (ProviderInstallSpec providerInstallSpec : providers) { |
| providerInstallSpec.installProvider(); |
| } |
| |
| try (PasswordRetriever passwordRetriever = new PasswordRetriever()) { |
| // populate SignerConfig for old signer |
| oldSignerParams.setName("old signer"); |
| loadPrivateKeyAndCerts(oldSignerParams, passwordRetriever); |
| SigningCertificateLineage.SignerConfig oldSignerConfig = |
| new SigningCertificateLineage.SignerConfig.Builder( |
| oldSignerParams.getPrivateKey(), oldSignerParams.getCerts().get(0)) |
| .build(); |
| |
| // TOOD: don't require private key |
| newSignerParams.setName("new signer"); |
| loadPrivateKeyAndCerts(newSignerParams, passwordRetriever); |
| SigningCertificateLineage.SignerConfig newSignerConfig = |
| new SigningCertificateLineage.SignerConfig.Builder( |
| newSignerParams.getPrivateKey(), newSignerParams.getCerts().get(0)) |
| .build(); |
| |
| // ok we're all set up, let's rotate! |
| SigningCertificateLineage lineage; |
| if (inputKeyLineage != null) { |
| // we already have history, add the new key to the end of it |
| lineage = getLineageFromInputFile(inputKeyLineage); |
| lineage.updateSignerCapabilities( |
| oldSignerConfig, oldSignerParams.getSignerCapabilitiesBuilder().build()); |
| lineage = |
| lineage.spawnDescendant( |
| oldSignerConfig, |
| newSignerConfig, |
| newSignerParams.getSignerCapabilitiesBuilder().build()); |
| } else { |
| // this is the first entry in our signing history, create a new one from the old and |
| // new signer info |
| lineage = |
| new SigningCertificateLineage.Builder(oldSignerConfig, newSignerConfig) |
| .setMinSdkVersion(minSdkVersion) |
| .setOriginalCapabilities( |
| oldSignerParams.getSignerCapabilitiesBuilder().build()) |
| .setNewCapabilities( |
| newSignerParams.getSignerCapabilitiesBuilder().build()) |
| .build(); |
| } |
| // and write out the result |
| lineage.writeToFile(outputKeyLineage); |
| } |
| if (verbose) { |
| System.out.println("Rotation entry generated."); |
| } |
| } |
| |
| public static void lineage(String[] params) throws Exception { |
| if (params.length == 0) { |
| printUsage(HELP_PAGE_LINEAGE); |
| return; |
| } |
| |
| boolean verbose = false; |
| boolean printCerts = false; |
| boolean lineageUpdated = false; |
| File inputKeyLineage = null; |
| File outputKeyLineage = null; |
| String optionName; |
| OptionsParser optionsParser = new OptionsParser(params); |
| List<SignerParams> signers = new ArrayList<>(1); |
| while ((optionName = optionsParser.nextOption()) != null) { |
| if (("help".equals(optionName)) || ("h".equals(optionName))) { |
| printUsage(HELP_PAGE_LINEAGE); |
| return; |
| } else if ("in".equals(optionName)) { |
| inputKeyLineage = new File(optionsParser.getRequiredValue("Input file name")); |
| } else if ("out".equals(optionName)) { |
| outputKeyLineage = new File(optionsParser.getRequiredValue("Output file name")); |
| } else if ("signer".equals(optionName)) { |
| SignerParams signerParams = processSignerParams(optionsParser); |
| signers.add(signerParams); |
| } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { |
| verbose = optionsParser.getOptionalBooleanValue(true); |
| } else if ("print-certs".equals(optionName)) { |
| printCerts = optionsParser.getOptionalBooleanValue(true); |
| } else { |
| throw new ParameterException( |
| "Unsupported option: " + optionsParser.getOptionOriginalForm() |
| + ". See --help for supported options."); |
| } |
| } |
| if (inputKeyLineage == null) { |
| throw new ParameterException("Input lineage file parameter not present"); |
| } |
| SigningCertificateLineage lineage = getLineageFromInputFile(inputKeyLineage); |
| |
| try (PasswordRetriever passwordRetriever = new PasswordRetriever()) { |
| for (int i = 0; i < signers.size(); i++) { |
| SignerParams signerParams = signers.get(i); |
| signerParams.setName("signer #" + (i + 1)); |
| loadPrivateKeyAndCerts(signerParams, passwordRetriever); |
| SigningCertificateLineage.SignerConfig signerConfig = |
| new SigningCertificateLineage.SignerConfig.Builder( |
| signerParams.getPrivateKey(), signerParams.getCerts().get(0)) |
| .build(); |
| try { |
| // since only the caller specified capabilities will be updated a direct |
| // comparison between the original capabilities of the signer and the |
| // signerCapabilitiesBuilder object with potential default values is not |
| // possible. Instead the capabilities should be updated first, then the new |
| // capabilities can be compared against the original to determine if the |
| // lineage has been updated and needs to be written out to a file. |
| SignerCapabilities origCapabilities = lineage.getSignerCapabilities( |
| signerConfig); |
| lineage.updateSignerCapabilities( |
| signerConfig, signerParams.getSignerCapabilitiesBuilder().build()); |
| SignerCapabilities newCapabilities = lineage.getSignerCapabilities( |
| signerConfig); |
| if (origCapabilities.equals(newCapabilities)) { |
| if (verbose) { |
| System.out.println( |
| "The provided signer capabilities for " |
| + signerParams.getName() |
| + " are unchanged."); |
| } |
| } else { |
| lineageUpdated = true; |
| if (verbose) { |
| System.out.println( |
| "Updated signer capabilities for " + signerParams.getName() |
| + "."); |
| } |
| } |
| } catch (IllegalArgumentException e) { |
| throw new ParameterException( |
| "The signer " + signerParams.getName() |
| + " was not found in the specified lineage."); |
| } |
| } |
| } |
| if (printCerts) { |
| List<X509Certificate> signingCerts = lineage.getCertificatesInLineage(); |
| for (int i = 0; i < signingCerts.size(); i++) { |
| X509Certificate signerCert = signingCerts.get(i); |
| SignerCapabilities signerCapabilities = lineage.getSignerCapabilities(signerCert); |
| printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose); |
| printCapabilities(signerCapabilities); |
| } |
| } |
| if (lineageUpdated) { |
| if (outputKeyLineage != null) { |
| lineage.writeToFile(outputKeyLineage); |
| if (verbose) { |
| System.out.println("Updated lineage saved to " + outputKeyLineage + "."); |
| } |
| } else { |
| throw new ParameterException( |
| "The lineage was modified but an output file for the lineage was not " |
| + "specified"); |
| } |
| } |
| } |
| |
| /** |
| * Extracts the Signing Certificate Lineage from the provided lineage or APK file. |
| */ |
| private static SigningCertificateLineage getLineageFromInputFile(File inputLineageFile) |
| throws ParameterException { |
| try (RandomAccessFile f = new RandomAccessFile(inputLineageFile, "r")) { |
| if (f.length() < 4) { |
| throw new ParameterException("The input file is not a valid lineage file."); |
| } |
| DataSource apk = DataSources.asDataSource(f); |
| int magicValue = apk.getByteBuffer(0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); |
| if (magicValue == SigningCertificateLineage.MAGIC) { |
| return SigningCertificateLineage.readFromFile(inputLineageFile); |
| } else if (magicValue == ZIP_MAGIC) { |
| return SigningCertificateLineage.readFromApkFile(inputLineageFile); |
| } else { |
| throw new ParameterException("The input file is not a valid lineage file."); |
| } |
| } catch (IOException | ApkFormatException | IllegalArgumentException e) { |
| throw new ParameterException(e.getMessage()); |
| } |
| } |
| |
| private static SignerParams processSignerParams(OptionsParser optionsParser) |
| throws OptionsParser.OptionsException, ParameterException { |
| SignerParams signerParams = new SignerParams(); |
| String optionName; |
| while ((optionName = optionsParser.nextOption()) != null) { |
| if ("ks".equals(optionName)) { |
| signerParams.setKeystoreFile(optionsParser.getRequiredValue("KeyStore file")); |
| } else if ("ks-key-alias".equals(optionName)) { |
| signerParams.setKeystoreKeyAlias( |
| optionsParser.getRequiredValue("KeyStore key alias")); |
| } else if ("ks-pass".equals(optionName)) { |
| signerParams.setKeystorePasswordSpec( |
| optionsParser.getRequiredValue("KeyStore password")); |
| } else if ("key-pass".equals(optionName)) { |
| signerParams.setKeyPasswordSpec(optionsParser.getRequiredValue("Key password")); |
| } else if ("pass-encoding".equals(optionName)) { |
| String charsetName = |
| optionsParser.getRequiredValue("Password character encoding"); |
| try { |
| signerParams.setPasswordCharset( |
| PasswordRetriever.getCharsetByName(charsetName)); |
| } catch (IllegalArgumentException e) { |
| throw new ParameterException( |
| "Unsupported password character encoding requested using" |
| + " --pass-encoding: " + charsetName); |
| } |
| } else if ("ks-type".equals(optionName)) { |
| signerParams.setKeystoreType(optionsParser.getRequiredValue("KeyStore type")); |
| } else if ("ks-provider-name".equals(optionName)) { |
| signerParams.setKeystoreProviderName( |
| optionsParser.getRequiredValue("JCA KeyStore Provider name")); |
| } else if ("ks-provider-class".equals(optionName)) { |
| signerParams.setKeystoreProviderClass( |
| optionsParser.getRequiredValue("JCA KeyStore Provider class name")); |
| } else if ("ks-provider-arg".equals(optionName)) { |
| signerParams.setKeystoreProviderArg( |
| optionsParser.getRequiredValue( |
| "JCA KeyStore Provider constructor argument")); |
| } else if ("key".equals(optionName)) { |
| signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file")); |
| } else if ("cert".equals(optionName)) { |
| signerParams.setCertFile(optionsParser.getRequiredValue("Certificate file")); |
| } else if ("set-installed-data".equals(optionName)) { |
| signerParams |
| .getSignerCapabilitiesBuilder() |
| .setInstalledData(optionsParser.getOptionalBooleanValue(true)); |
| } else if ("set-shared-uid".equals(optionName)) { |
| signerParams |
| .getSignerCapabilitiesBuilder() |
| .setSharedUid(optionsParser.getOptionalBooleanValue(true)); |
| } else if ("set-permission".equals(optionName)) { |
| signerParams |
| .getSignerCapabilitiesBuilder() |
| .setPermission(optionsParser.getOptionalBooleanValue(true)); |
| } else if ("set-rollback".equals(optionName)) { |
| signerParams |
| .getSignerCapabilitiesBuilder() |
| .setRollback(optionsParser.getOptionalBooleanValue(true)); |
| } else if ("set-auth".equals(optionName)) { |
| signerParams |
| .getSignerCapabilitiesBuilder() |
| .setAuth(optionsParser.getOptionalBooleanValue(true)); |
| } else { |
| // not a signer option, reset optionsParser and let caller deal with it |
| optionsParser.putOption(); |
| break; |
| } |
| } |
| |
| if (signerParams.isEmpty()) { |
| throw new ParameterException("Signer specified without arguments"); |
| } |
| return signerParams; |
| } |
| |
| private static void printUsage(String page) { |
| try (BufferedReader in = |
| new BufferedReader( |
| new InputStreamReader( |
| ApkSignerTool.class.getResourceAsStream(page), |
| StandardCharsets.UTF_8))) { |
| String line; |
| while ((line = in.readLine()) != null) { |
| System.out.println(line); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Failed to read " + page + " resource"); |
| } |
| } |
| |
| /** |
| * Prints details from the provided certificate to stdout. |
| * |
| * @param cert the certificate to be displayed. |
| * @param name the name to be used to identify the certificate. |
| * @param verbose boolean indicating whether public key details from the certificate should be |
| * displayed. |
| * @throws NoSuchAlgorithmException if an instance of MD5, SHA-1, or SHA-256 cannot be |
| * obtained. |
| * @throws CertificateEncodingException if an error is encountered when encoding the |
| * certificate. |
| */ |
| public static void printCertificate(X509Certificate cert, String name, boolean verbose) |
| throws NoSuchAlgorithmException, CertificateEncodingException { |
| if (cert == null) { |
| throw new NullPointerException("cert == null"); |
| } |
| if (sha256 == null || sha1 == null || md5 == null) { |
| sha256 = MessageDigest.getInstance("SHA-256"); |
| sha1 = MessageDigest.getInstance("SHA-1"); |
| md5 = MessageDigest.getInstance("MD5"); |
| } |
| System.out.println(name + " certificate DN: " + cert.getSubjectDN()); |
| byte[] encodedCert = cert.getEncoded(); |
| System.out.println(name + " certificate SHA-256 digest: " + HexEncoding.encode( |
| sha256.digest(encodedCert))); |
| System.out.println(name + " certificate SHA-1 digest: " + HexEncoding.encode( |
| sha1.digest(encodedCert))); |
| System.out.println( |
| name + " certificate MD5 digest: " + HexEncoding.encode(md5.digest(encodedCert))); |
| if (verbose) { |
| PublicKey publicKey = cert.getPublicKey(); |
| System.out.println(name + " key algorithm: " + publicKey.getAlgorithm()); |
| int keySize = -1; |
| if (publicKey instanceof RSAKey) { |
| keySize = ((RSAKey) publicKey).getModulus().bitLength(); |
| } else if (publicKey instanceof ECKey) { |
| keySize = ((ECKey) publicKey).getParams() |
| .getOrder().bitLength(); |
| } else if (publicKey instanceof DSAKey) { |
| // DSA parameters may be inherited from the certificate. We |
| // don't handle this case at the moment. |
| DSAParams dsaParams = ((DSAKey) publicKey).getParams(); |
| if (dsaParams != null) { |
| keySize = dsaParams.getP().bitLength(); |
| } |
| } |
| System.out.println( |
| name + " key size (bits): " + ((keySize != -1) ? String.valueOf(keySize) |
| : "n/a")); |
| byte[] encodedKey = publicKey.getEncoded(); |
| System.out.println(name + " public key SHA-256 digest: " + HexEncoding.encode( |
| sha256.digest(encodedKey))); |
| System.out.println(name + " public key SHA-1 digest: " + HexEncoding.encode( |
| sha1.digest(encodedKey))); |
| System.out.println( |
| name + " public key MD5 digest: " + HexEncoding.encode(md5.digest(encodedKey))); |
| } |
| } |
| |
| /** |
| * Prints the capabilities of the provided object to stdout. Each of the potential |
| * capabilities is displayed along with a boolean indicating whether this object has |
| * that capability. |
| */ |
| public static void printCapabilities(SignerCapabilities capabilities) { |
| System.out.println("Has installed data capability: " + capabilities.hasInstalledData()); |
| System.out.println("Has shared UID capability : " + capabilities.hasSharedUid()); |
| System.out.println("Has permission capability : " + capabilities.hasPermission()); |
| System.out.println("Has rollback capability : " + capabilities.hasRollback()); |
| System.out.println("Has auth capability : " + capabilities.hasAuth()); |
| } |
| |
| private static class ProviderInstallSpec { |
| String className; |
| String constructorParam; |
| Integer position; |
| |
| private boolean isEmpty() { |
| return (className == null) && (constructorParam == null) && (position == null); |
| } |
| |
| private void installProvider() throws Exception { |
| if (className == null) { |
| throw new ParameterException( |
| "JCA Provider class name (--provider-class) must be specified"); |
| } |
| |
| Class<?> providerClass = Class.forName(className); |
| if (!Provider.class.isAssignableFrom(providerClass)) { |
| throw new ParameterException( |
| "JCA Provider class " + providerClass + " not subclass of " |
| + Provider.class.getName()); |
| } |
| Provider provider; |
| if (constructorParam != null) { |
| try { |
| // Single-arg Provider constructor |
| provider = |
| (Provider) providerClass.getConstructor(String.class) |
| .newInstance(constructorParam); |
| } catch (NoSuchMethodException e) { |
| // Starting from JDK 9 the single-arg constructor accepting the configuration |
| // has been replaced by a configure(String) method to be invoked after |
| // instantiating the Provider with the no-arg constructor. |
| provider = (Provider) providerClass.getConstructor().newInstance(); |
| provider = (Provider) providerClass.getMethod("configure", String.class) |
| .invoke(provider, constructorParam); |
| } |
| } else { |
| // No-arg Provider constructor |
| provider = (Provider) providerClass.getConstructor().newInstance(); |
| } |
| |
| if (position == null) { |
| Security.addProvider(provider); |
| } else { |
| Security.insertProviderAt(provider, position); |
| } |
| } |
| } |
| |
| /** |
| * Loads the private key and certificates from either the specified keystore or files specified |
| * in the signer params using the provided passwordRetriever. |
| * |
| * @throws ParameterException if any errors are encountered when attempting to load |
| * the private key and certificates. |
| */ |
| private static void loadPrivateKeyAndCerts(SignerParams params, |
| PasswordRetriever passwordRetriever) throws ParameterException { |
| try { |
| params.loadPrivateKeyAndCerts(passwordRetriever); |
| if (params.getKeystoreKeyAlias() != null) { |
| params.setName(params.getKeystoreKeyAlias()); |
| } else if (params.getKeyFile() != null) { |
| String keyFileName = new File(params.getKeyFile()).getName(); |
| int delimiterIndex = keyFileName.indexOf('.'); |
| if (delimiterIndex == -1) { |
| params.setName(keyFileName); |
| } else { |
| params.setName(keyFileName.substring(0, delimiterIndex)); |
| } |
| } else { |
| throw new RuntimeException( |
| "Neither KeyStore key alias nor private key file available for " |
| + params.getName()); |
| } |
| } catch (ParameterException e) { |
| throw new ParameterException( |
| "Failed to load signer \"" + params.getName() + "\":" + e.getMessage()); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| throw new ParameterException("Failed to load signer \"" + params.getName() + "\""); |
| } |
| } |
| } |