| /* |
| * 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.apk.MinSdkVersionException; |
| import java.io.BufferedReader; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.StandardCopyOption; |
| import java.security.InvalidKeyException; |
| import java.security.Key; |
| import java.security.KeyFactory; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.Provider; |
| import java.security.PublicKey; |
| import java.security.Security; |
| import java.security.UnrecoverableKeyException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateFactory; |
| 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.security.spec.InvalidKeySpecException; |
| import java.security.spec.PKCS8EncodedKeySpec; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.List; |
| import javax.crypto.EncryptedPrivateKeyInfo; |
| import javax.crypto.SecretKey; |
| import javax.crypto.SecretKeyFactory; |
| import javax.crypto.spec.PBEKeySpec; |
| |
| /** |
| * 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.8"; |
| 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"; |
| |
| 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; |
| } |
| |
| 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 ("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; |
| } |
| } |
| |
| 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; |
| int minSdkVersion = 1; |
| boolean minSdkVersionSpecified = false; |
| int maxSdkVersion = Integer.MAX_VALUE; |
| List<SignerParams> signers = new ArrayList<>(1); |
| SignerParams signerParams = new SignerParams(); |
| 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_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 ("next-signer".equals(optionName)) { |
| if (!signerParams.isEmpty()) { |
| signers.add(signerParams); |
| signerParams = new SignerParams(); |
| } |
| } else if ("ks".equals(optionName)) { |
| signerParams.keystoreFile = optionsParser.getRequiredValue("KeyStore file"); |
| } else if ("ks-key-alias".equals(optionName)) { |
| signerParams.keystoreKeyAlias = |
| optionsParser.getRequiredValue("KeyStore key alias"); |
| } else if ("ks-pass".equals(optionName)) { |
| signerParams.keystorePasswordSpec = |
| optionsParser.getRequiredValue("KeyStore password"); |
| } else if ("key-pass".equals(optionName)) { |
| signerParams.keyPasswordSpec = optionsParser.getRequiredValue("Key password"); |
| } else if ("pass-encoding".equals(optionName)) { |
| String charsetName = |
| optionsParser.getRequiredValue("Password character encoding"); |
| try { |
| signerParams.passwordCharset = 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.v1SigFileBasename = |
| optionsParser.getRequiredValue("JAR signature file basename"); |
| } else if ("ks-type".equals(optionName)) { |
| signerParams.keystoreType = optionsParser.getRequiredValue("KeyStore type"); |
| } else if ("ks-provider-name".equals(optionName)) { |
| signerParams.keystoreProviderName = |
| optionsParser.getRequiredValue("JCA KeyStore Provider name"); |
| } else if ("ks-provider-class".equals(optionName)) { |
| signerParams.keystoreProviderClass = |
| optionsParser.getRequiredValue("JCA KeyStore Provider class name"); |
| } else if ("ks-provider-arg".equals(optionName)) { |
| signerParams.keystoreProviderArg = |
| optionsParser.getRequiredValue( |
| "JCA KeyStore Provider constructor argument"); |
| } else if ("key".equals(optionName)) { |
| signerParams.keyFile = optionsParser.getRequiredValue("Private key file"); |
| } else if ("cert".equals(optionName)) { |
| signerParams.certFile = optionsParser.getRequiredValue("Certificate file"); |
| } 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 (!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(); |
| } |
| |
| List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size()); |
| int signerNumber = 0; |
| try (PasswordRetriever passwordRetriever = new PasswordRetriever()) { |
| for (SignerParams signer : signers) { |
| signerNumber++; |
| signer.name = "signer #" + signerNumber; |
| try { |
| signer.loadPrivateKeyAndCerts(passwordRetriever); |
| } catch (ParameterException e) { |
| System.err.println( |
| "Failed to load signer \"" + signer.name + "\": " |
| + e.getMessage()); |
| System.exit(2); |
| return; |
| } catch (Exception e) { |
| System.err.println("Failed to load signer \"" + signer.name + "\""); |
| e.printStackTrace(); |
| System.exit(2); |
| return; |
| } |
| String v1SigBasename; |
| if (signer.v1SigFileBasename != null) { |
| v1SigBasename = signer.v1SigFileBasename; |
| } else if (signer.keystoreKeyAlias != null) { |
| v1SigBasename = signer.keystoreKeyAlias; |
| } else if (signer.keyFile != null) { |
| String keyFileName = new File(signer.keyFile).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.privateKey, signer.certs) |
| .build(); |
| signerConfigs.add(signerConfig); |
| } |
| } |
| |
| 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(false) |
| .setV1SigningEnabled(v1SigningEnabled) |
| .setV2SigningEnabled(v2SigningEnabled); |
| if (minSdkVersionSpecified) { |
| apkSignerBuilder.setMinSdkVersion(minSdkVersion); |
| } |
| 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 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; |
| OptionsParser optionsParser = new OptionsParser(params); |
| String optionName; |
| String optionOriginalForm = 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 ("in".equals(optionName)) { |
| inputApk = new File(optionsParser.getRequiredValue("Input APK file")); |
| } 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); |
| } |
| ApkVerifier apkVerifier = apkVerifierBuilder.build(); |
| ApkVerifier.Result result; |
| try { |
| result = 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(); |
| |
| 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("Number of signers: " + signerCerts.size()); |
| } |
| if (printCerts) { |
| int signerNumber = 0; |
| MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); |
| MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| MessageDigest md5 = MessageDigest.getInstance("MD5"); |
| for (X509Certificate signerCert : signerCerts) { |
| signerNumber++; |
| System.out.println( |
| "Signer #" + signerNumber + " certificate DN" |
| + ": " + signerCert.getSubjectDN()); |
| byte[] encodedCert = signerCert.getEncoded(); |
| System.out.println( |
| "Signer #" + signerNumber + " certificate SHA-256 digest: " |
| + HexEncoding.encode(sha256.digest(encodedCert))); |
| System.out.println( |
| "Signer #" + signerNumber + " certificate SHA-1 digest: " |
| + HexEncoding.encode(sha1.digest(encodedCert))); |
| System.out.println( |
| "Signer #" + signerNumber + " certificate MD5 digest: " |
| + HexEncoding.encode(md5.digest(encodedCert))); |
| if (verbose) { |
| PublicKey publicKey = signerCert.getPublicKey(); |
| System.out.println( |
| "Signer #" + signerNumber + " 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( |
| "Signer #" + signerNumber + " key size (bits): " |
| + ((keySize != -1) |
| ? String.valueOf(keySize) : "n/a")); |
| byte[] encodedKey = publicKey.getEncoded(); |
| System.out.println( |
| "Signer #" + signerNumber + " public key SHA-256 digest: " |
| + HexEncoding.encode(sha256.digest(encodedKey))); |
| System.out.println( |
| "Signer #" + signerNumber + " public key SHA-1 digest: " |
| + HexEncoding.encode(sha1.digest(encodedKey))); |
| System.out.println( |
| "Signer #" + signerNumber + " public key MD5 digest: " |
| + HexEncoding.encode(md5.digest(encodedKey))); |
| } |
| } |
| } |
| } 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); |
| } |
| } |
| |
| if (!verified) { |
| System.exit(1); |
| return; |
| } |
| if ((warningsTreatedAsErrors) && (warningsEncountered)) { |
| System.exit(1); |
| return; |
| } |
| } |
| |
| 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"); |
| } |
| } |
| |
| 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) { |
| // Single-arg Provider constructor |
| provider = |
| (Provider) providerClass.getConstructor(String.class) |
| .newInstance(constructorParam); |
| } else { |
| // No-arg Provider constructor |
| provider = (Provider) providerClass.getConstructor().newInstance(); |
| } |
| |
| if (position == null) { |
| Security.addProvider(provider); |
| } else { |
| Security.insertProviderAt(provider, position); |
| } |
| } |
| } |
| |
| private static class SignerParams { |
| String name; |
| |
| String keystoreFile; |
| String keystoreKeyAlias; |
| String keystorePasswordSpec; |
| String keyPasswordSpec; |
| Charset passwordCharset; |
| String keystoreType; |
| String keystoreProviderName; |
| String keystoreProviderClass; |
| String keystoreProviderArg; |
| |
| String keyFile; |
| String certFile; |
| |
| String v1SigFileBasename; |
| |
| PrivateKey privateKey; |
| List<X509Certificate> certs; |
| |
| private boolean isEmpty() { |
| return (name == null) |
| && (keystoreFile == null) |
| && (keystoreKeyAlias == null) |
| && (keystorePasswordSpec == null) |
| && (keyPasswordSpec == null) |
| && (passwordCharset == null) |
| && (keystoreType == null) |
| && (keystoreProviderName == null) |
| && (keystoreProviderClass == null) |
| && (keystoreProviderArg == null) |
| && (keyFile == null) |
| && (certFile == null) |
| && (v1SigFileBasename == null) |
| && (privateKey == null) |
| && (certs == null); |
| } |
| |
| private void loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever) throws Exception { |
| if (keystoreFile != null) { |
| if (keyFile != null) { |
| throw new ParameterException( |
| "--ks and --key may not be specified at the same time"); |
| } else if (certFile != null) { |
| throw new ParameterException( |
| "--ks and --cert may not be specified at the same time"); |
| } |
| loadPrivateKeyAndCertsFromKeyStore(passwordRetriever); |
| } else if (keyFile != null) { |
| loadPrivateKeyAndCertsFromFiles(passwordRetriever); |
| } else { |
| throw new ParameterException( |
| "KeyStore (--ks) or private key file (--key) must be specified"); |
| } |
| } |
| |
| private void loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever) |
| throws Exception { |
| if (keystoreFile == null) { |
| throw new ParameterException("KeyStore (--ks) must be specified"); |
| } |
| |
| // 1. Obtain a KeyStore implementation |
| String ksType = (keystoreType != null) ? keystoreType : KeyStore.getDefaultType(); |
| KeyStore ks; |
| if (keystoreProviderName != null) { |
| // Use a named Provider (assumes the provider is already installed) |
| ks = KeyStore.getInstance(ksType, keystoreProviderName); |
| } else if (keystoreProviderClass != null) { |
| // Use a new Provider instance (does not require the provider to be installed) |
| Class<?> ksProviderClass = Class.forName(keystoreProviderClass); |
| if (!Provider.class.isAssignableFrom(ksProviderClass)) { |
| throw new ParameterException( |
| "Keystore Provider class " + keystoreProviderClass + " not subclass of " |
| + Provider.class.getName()); |
| } |
| Provider ksProvider; |
| if (keystoreProviderArg != null) { |
| // Single-arg Provider constructor |
| ksProvider = |
| (Provider) ksProviderClass.getConstructor(String.class) |
| .newInstance(keystoreProviderArg); |
| } else { |
| // No-arg Provider constructor |
| ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); |
| } |
| ks = KeyStore.getInstance(ksType, ksProvider); |
| } else { |
| // Use the highest-priority Provider which offers the requested KeyStore type |
| ks = KeyStore.getInstance(ksType); |
| } |
| |
| // 2. Load the KeyStore |
| List<char[]> keystorePasswords; |
| Charset[] additionalPasswordEncodings; |
| { |
| String keystorePasswordSpec = |
| (this.keystorePasswordSpec != null) |
| ? this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN; |
| additionalPasswordEncodings = |
| (passwordCharset != null) |
| ? new Charset[] {passwordCharset} : new Charset[0]; |
| keystorePasswords = |
| passwordRetriever.getPasswords( |
| keystorePasswordSpec, |
| "Keystore password for " + name, |
| additionalPasswordEncodings); |
| loadKeyStoreFromFile( |
| ks, |
| "NONE".equals(keystoreFile) ? null : keystoreFile, |
| keystorePasswords); |
| } |
| |
| // 3. Load the PrivateKey and cert chain from KeyStore |
| String keyAlias = null; |
| PrivateKey key = null; |
| try { |
| if (keystoreKeyAlias == null) { |
| // Private key entry alias not specified. Find the key entry contained in this |
| // KeyStore. If the KeyStore contains multiple key entries, return an error. |
| Enumeration<String> aliases = ks.aliases(); |
| if (aliases != null) { |
| while (aliases.hasMoreElements()) { |
| String entryAlias = aliases.nextElement(); |
| if (ks.isKeyEntry(entryAlias)) { |
| keyAlias = entryAlias; |
| if (keystoreKeyAlias != null) { |
| throw new ParameterException( |
| keystoreFile + " contains multiple key entries" |
| + ". --ks-key-alias option must be used to specify" |
| + " which entry to use."); |
| } |
| keystoreKeyAlias = keyAlias; |
| } |
| } |
| } |
| if (keystoreKeyAlias == null) { |
| throw new ParameterException( |
| keystoreFile + " does not contain key entries"); |
| } |
| } |
| |
| // Private key entry alias known. Load that entry's private key. |
| keyAlias = keystoreKeyAlias; |
| if (!ks.isKeyEntry(keyAlias)) { |
| throw new ParameterException( |
| keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); |
| } |
| |
| Key entryKey; |
| if (keyPasswordSpec != null) { |
| // Key password spec is explicitly specified. Use this spec to obtain the |
| // password and then load the key using that password. |
| List<char[]> keyPasswords = |
| passwordRetriever.getPasswords( |
| keyPasswordSpec, |
| "Key \"" + keyAlias + "\" password for " + name, |
| additionalPasswordEncodings); |
| entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); |
| } else { |
| // Key password spec is not specified. This means we should assume that key |
| // password is the same as the keystore password and that, if this assumption is |
| // wrong, we should prompt for key password and retry loading the key using that |
| // password. |
| try { |
| entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords); |
| } catch (UnrecoverableKeyException expected) { |
| List<char[]> keyPasswords = |
| passwordRetriever.getPasswords( |
| PasswordRetriever.SPEC_STDIN, |
| "Key \"" + keyAlias + "\" password for " + name, |
| additionalPasswordEncodings); |
| entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); |
| } |
| } |
| |
| if (entryKey == null) { |
| throw new ParameterException( |
| keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); |
| } else if (!(entryKey instanceof PrivateKey)) { |
| throw new ParameterException( |
| keystoreFile + " entry \"" + keyAlias + "\" does not contain a private" |
| + " key. It contains a key of algorithm: " |
| + entryKey.getAlgorithm()); |
| } |
| key = (PrivateKey) entryKey; |
| } catch (UnrecoverableKeyException e) { |
| throw new IOException( |
| "Failed to obtain key with alias \"" + keyAlias + "\" from " + keystoreFile |
| + ". Wrong password?", |
| e); |
| } |
| this.privateKey = key; |
| Certificate[] certChain = ks.getCertificateChain(keyAlias); |
| if ((certChain == null) || (certChain.length == 0)) { |
| throw new ParameterException( |
| keystoreFile + " entry \"" + keyAlias + "\" does not contain certificates"); |
| } |
| this.certs = new ArrayList<>(certChain.length); |
| for (Certificate cert : certChain) { |
| this.certs.add((X509Certificate) cert); |
| } |
| } |
| |
| /** |
| * Loads the password-protected keystore from storage. |
| * |
| * @param file file backing the keystore or {@code null} if the keystore is not file-backed, |
| * for example, a PKCS #11 KeyStore. |
| */ |
| private static void loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords) |
| throws Exception { |
| Exception lastFailure = null; |
| for (char[] password : passwords) { |
| try { |
| if (file != null) { |
| try (FileInputStream in = new FileInputStream(file)) { |
| ks.load(in, password); |
| } |
| } else { |
| ks.load(null, password); |
| } |
| return; |
| } catch (Exception e) { |
| lastFailure = e; |
| } |
| } |
| if (lastFailure == null) { |
| throw new RuntimeException("No keystore passwords"); |
| } else { |
| throw lastFailure; |
| } |
| } |
| |
| private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords) |
| throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { |
| UnrecoverableKeyException lastFailure = null; |
| for (char[] password : passwords) { |
| try { |
| return ks.getKey(keyAlias, password); |
| } catch (UnrecoverableKeyException e) { |
| lastFailure = e; |
| } |
| } |
| if (lastFailure == null) { |
| throw new RuntimeException("No key passwords"); |
| } else { |
| throw lastFailure; |
| } |
| } |
| |
| private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriver) |
| throws Exception { |
| if (keyFile == null) { |
| throw new ParameterException("Private key file (--key) must be specified"); |
| } |
| if (certFile == null) { |
| throw new ParameterException("Certificate file (--cert) must be specified"); |
| } |
| byte[] privateKeyBlob = readFully(new File(keyFile)); |
| |
| PKCS8EncodedKeySpec keySpec; |
| // Potentially encrypted key blob |
| try { |
| EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = |
| new EncryptedPrivateKeyInfo(privateKeyBlob); |
| |
| // The blob is indeed an encrypted private key blob |
| String passwordSpec = |
| (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN; |
| Charset[] additionalPasswordEncodings = |
| (passwordCharset != null) |
| ? new Charset[] {passwordCharset} : new Charset[0]; |
| List<char[]> keyPasswords = |
| passwordRetriver.getPasswords( |
| passwordSpec, |
| "Private key password for " + name, |
| additionalPasswordEncodings); |
| keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords); |
| } catch (IOException e) { |
| // The blob is not an encrypted private key blob |
| if (keyPasswordSpec == null) { |
| // Given that no password was specified, assume the blob is an unencrypted |
| // private key blob |
| keySpec = new PKCS8EncodedKeySpec(privateKeyBlob); |
| } else { |
| throw new InvalidKeySpecException( |
| "Failed to parse encrypted private key blob " + keyFile, e); |
| } |
| } |
| |
| // Load the private key from its PKCS #8 encoded form. |
| try { |
| privateKey = loadPkcs8EncodedPrivateKey(keySpec); |
| } catch (InvalidKeySpecException e) { |
| throw new InvalidKeySpecException( |
| "Failed to load PKCS #8 encoded private key from " + keyFile, e); |
| } |
| |
| // Load certificates |
| Collection<? extends Certificate> certs; |
| try (FileInputStream in = new FileInputStream(certFile)) { |
| certs = CertificateFactory.getInstance("X.509").generateCertificates(in); |
| } |
| List<X509Certificate> certList = new ArrayList<>(certs.size()); |
| for (Certificate cert : certs) { |
| certList.add((X509Certificate) cert); |
| } |
| this.certs = certList; |
| } |
| |
| private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey( |
| EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords) |
| throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { |
| SecretKeyFactory keyFactory = |
| SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); |
| InvalidKeySpecException lastKeySpecException = null; |
| InvalidKeyException lastKeyException = null; |
| for (char[] password : passwords) { |
| PBEKeySpec decryptionKeySpec = new PBEKeySpec(password); |
| try { |
| SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec); |
| return encryptedPrivateKeyInfo.getKeySpec(decryptionKey); |
| } catch (InvalidKeySpecException e) { |
| lastKeySpecException = e; |
| } catch (InvalidKeyException e) { |
| lastKeyException = e; |
| } |
| } |
| if ((lastKeyException == null) && (lastKeySpecException == null)) { |
| throw new RuntimeException("No passwords"); |
| } else if (lastKeyException != null) { |
| throw lastKeyException; |
| } else { |
| throw lastKeySpecException; |
| } |
| } |
| |
| private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec) |
| throws InvalidKeySpecException, NoSuchAlgorithmException { |
| try { |
| return KeyFactory.getInstance("RSA").generatePrivate(spec); |
| } catch (InvalidKeySpecException expected) { |
| } |
| try { |
| return KeyFactory.getInstance("EC").generatePrivate(spec); |
| } catch (InvalidKeySpecException expected) { |
| } |
| try { |
| return KeyFactory.getInstance("DSA").generatePrivate(spec); |
| } catch (InvalidKeySpecException expected) { |
| } |
| throw new InvalidKeySpecException("Not an RSA, EC, or DSA private key"); |
| } |
| } |
| |
| private static byte[] readFully(File file) throws IOException { |
| ByteArrayOutputStream result = new ByteArrayOutputStream(); |
| try (FileInputStream in = new FileInputStream(file)) { |
| drain(in, result); |
| } |
| return result.toByteArray(); |
| } |
| |
| private static void drain(InputStream in, OutputStream out) throws IOException { |
| byte[] buf = new byte[65536]; |
| int chunkSize; |
| while ((chunkSize = in.read(buf)) != -1) { |
| out.write(buf, 0, chunkSize); |
| } |
| } |
| |
| /** |
| * Indicates that there is an issue with command-line parameters provided to this tool. |
| */ |
| private static class ParameterException extends Exception { |
| private static final long serialVersionUID = 1L; |
| |
| ParameterException(String message) { |
| super(message); |
| } |
| } |
| } |