1 /* 2 * Copyright (C) 2019 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.SigningCertificateLineage; 20 import com.android.apksig.SigningCertificateLineage.SignerCapabilities; 21 import com.android.apksig.internal.util.X509CertificateUtils; 22 23 import java.io.ByteArrayOutputStream; 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.io.OutputStream; 29 import java.nio.charset.Charset; 30 import java.security.InvalidKeyException; 31 import java.security.Key; 32 import java.security.KeyFactory; 33 import java.security.KeyStore; 34 import java.security.KeyStoreException; 35 import java.security.NoSuchAlgorithmException; 36 import java.security.PrivateKey; 37 import java.security.Provider; 38 import java.security.UnrecoverableKeyException; 39 import java.security.cert.Certificate; 40 import java.security.cert.X509Certificate; 41 import java.security.spec.InvalidKeySpecException; 42 import java.security.spec.PKCS8EncodedKeySpec; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.Enumeration; 46 import java.util.List; 47 import javax.crypto.EncryptedPrivateKeyInfo; 48 import javax.crypto.SecretKey; 49 import javax.crypto.SecretKeyFactory; 50 import javax.crypto.spec.PBEKeySpec; 51 52 /** A utility class to load private key and certificates from a keystore or key and cert files. */ 53 public class SignerParams { 54 private String name; 55 56 private String keystoreFile; 57 private String keystoreKeyAlias; 58 private String keystorePasswordSpec; 59 private String keyPasswordSpec; 60 private Charset passwordCharset; 61 private String keystoreType; 62 private String keystoreProviderName; 63 private String keystoreProviderClass; 64 private String keystoreProviderArg; 65 66 private String keyFile; 67 private String certFile; 68 69 private String v1SigFileBasename; 70 71 private PrivateKey privateKey; 72 private List<X509Certificate> certs; 73 private final SignerCapabilities.Builder signerCapabilitiesBuilder = 74 new SignerCapabilities.Builder(); 75 76 private int minSdkVersion; 77 private SigningCertificateLineage signingCertificateLineage; 78 getName()79 public String getName() { 80 return name; 81 } 82 setName(String name)83 public void setName(String name) { 84 this.name = name; 85 } 86 setKeystoreFile(String keystoreFile)87 public void setKeystoreFile(String keystoreFile) { 88 this.keystoreFile = keystoreFile; 89 } 90 getKeystoreKeyAlias()91 public String getKeystoreKeyAlias() { 92 return keystoreKeyAlias; 93 } 94 setKeystoreKeyAlias(String keystoreKeyAlias)95 public void setKeystoreKeyAlias(String keystoreKeyAlias) { 96 this.keystoreKeyAlias = keystoreKeyAlias; 97 } 98 setKeystorePasswordSpec(String keystorePasswordSpec)99 public void setKeystorePasswordSpec(String keystorePasswordSpec) { 100 this.keystorePasswordSpec = keystorePasswordSpec; 101 } 102 setKeyPasswordSpec(String keyPasswordSpec)103 public void setKeyPasswordSpec(String keyPasswordSpec) { 104 this.keyPasswordSpec = keyPasswordSpec; 105 } 106 setPasswordCharset(Charset passwordCharset)107 public void setPasswordCharset(Charset passwordCharset) { 108 this.passwordCharset = passwordCharset; 109 } 110 setKeystoreType(String keystoreType)111 public void setKeystoreType(String keystoreType) { 112 this.keystoreType = keystoreType; 113 } 114 setKeystoreProviderName(String keystoreProviderName)115 public void setKeystoreProviderName(String keystoreProviderName) { 116 this.keystoreProviderName = keystoreProviderName; 117 } 118 setKeystoreProviderClass(String keystoreProviderClass)119 public void setKeystoreProviderClass(String keystoreProviderClass) { 120 this.keystoreProviderClass = keystoreProviderClass; 121 } 122 setKeystoreProviderArg(String keystoreProviderArg)123 public void setKeystoreProviderArg(String keystoreProviderArg) { 124 this.keystoreProviderArg = keystoreProviderArg; 125 } 126 getKeyFile()127 public String getKeyFile() { 128 return keyFile; 129 } 130 setKeyFile(String keyFile)131 public void setKeyFile(String keyFile) { 132 this.keyFile = keyFile; 133 } 134 setCertFile(String certFile)135 public void setCertFile(String certFile) { 136 this.certFile = certFile; 137 } 138 getV1SigFileBasename()139 public String getV1SigFileBasename() { 140 return v1SigFileBasename; 141 } 142 setV1SigFileBasename(String v1SigFileBasename)143 public void setV1SigFileBasename(String v1SigFileBasename) { 144 this.v1SigFileBasename = v1SigFileBasename; 145 } 146 getPrivateKey()147 public PrivateKey getPrivateKey() { 148 return privateKey; 149 } 150 getCerts()151 public List<X509Certificate> getCerts() { 152 return certs; 153 } 154 getSignerCapabilitiesBuilder()155 public SignerCapabilities.Builder getSignerCapabilitiesBuilder() { 156 return signerCapabilitiesBuilder; 157 } 158 getMinSdkVersion()159 public int getMinSdkVersion() { 160 return minSdkVersion; 161 } 162 setMinSdkVersion(int minSdkVersion)163 public void setMinSdkVersion(int minSdkVersion) { 164 this.minSdkVersion = minSdkVersion; 165 } 166 getSigningCertificateLineage()167 public SigningCertificateLineage getSigningCertificateLineage() { 168 return signingCertificateLineage; 169 } 170 setSigningCertificateLineage(SigningCertificateLineage lineage)171 public void setSigningCertificateLineage(SigningCertificateLineage lineage) { 172 this.signingCertificateLineage = lineage; 173 } 174 isEmpty()175 boolean isEmpty() { 176 return (name == null) 177 && (keystoreFile == null) 178 && (keystoreKeyAlias == null) 179 && (keystorePasswordSpec == null) 180 && (keyPasswordSpec == null) 181 && (passwordCharset == null) 182 && (keystoreType == null) 183 && (keystoreProviderName == null) 184 && (keystoreProviderClass == null) 185 && (keystoreProviderArg == null) 186 && (keyFile == null) 187 && (certFile == null) 188 && (v1SigFileBasename == null) 189 && (privateKey == null) 190 && (certs == null); 191 } 192 loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever)193 public void loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever) throws Exception { 194 if (keystoreFile != null) { 195 if (keyFile != null) { 196 throw new ParameterException( 197 "--ks and --key may not be specified at the same time"); 198 } else if (certFile != null) { 199 throw new ParameterException( 200 "--ks and --cert may not be specified at the same time"); 201 } 202 loadPrivateKeyAndCertsFromKeyStore(passwordRetriever); 203 } else if (keyFile != null) { 204 loadPrivateKeyAndCertsFromFiles(passwordRetriever); 205 } else { 206 throw new ParameterException( 207 "KeyStore (--ks) or private key file (--key) must be specified"); 208 } 209 } 210 loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever)211 private void loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever) 212 throws Exception { 213 if (keystoreFile == null) { 214 throw new ParameterException("KeyStore (--ks) must be specified"); 215 } 216 217 // 1. Obtain a KeyStore implementation 218 String ksType = (keystoreType != null) ? keystoreType : KeyStore.getDefaultType(); 219 KeyStore ks; 220 if (keystoreProviderName != null) { 221 // Use a named Provider (assumes the provider is already installed) 222 ks = KeyStore.getInstance(ksType, keystoreProviderName); 223 } else if (keystoreProviderClass != null) { 224 // Use a new Provider instance (does not require the provider to be installed) 225 Class<?> ksProviderClass = Class.forName(keystoreProviderClass); 226 if (!Provider.class.isAssignableFrom(ksProviderClass)) { 227 throw new ParameterException( 228 "Keystore Provider class " + keystoreProviderClass + " not subclass of " 229 + Provider.class.getName()); 230 } 231 Provider ksProvider; 232 if (keystoreProviderArg != null) { 233 try { 234 // Single-arg Provider constructor 235 ksProvider = 236 (Provider) ksProviderClass.getConstructor(String.class) 237 .newInstance(keystoreProviderArg); 238 } catch (NoSuchMethodException e) { 239 // Starting from JDK 9 the single-arg constructor accepting the configuration 240 // has been replaced by a configure(String) method to be invoked after 241 // instantiating the Provider with the no-arg constructor. 242 ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); 243 ksProvider = (Provider) ksProviderClass.getMethod("configure", 244 String.class).invoke(ksProvider, keystoreProviderArg); 245 } 246 } else { 247 // No-arg Provider constructor 248 ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); 249 } 250 ks = KeyStore.getInstance(ksType, ksProvider); 251 } else { 252 // Use the highest-priority Provider which offers the requested KeyStore type 253 ks = KeyStore.getInstance(ksType); 254 } 255 256 // 2. Load the KeyStore 257 List<char[]> keystorePasswords; 258 Charset[] additionalPasswordEncodings; 259 { 260 String keystorePasswordSpec = 261 (this.keystorePasswordSpec != null) 262 ? this.keystorePasswordSpec 263 : PasswordRetriever.SPEC_STDIN; 264 additionalPasswordEncodings = 265 (passwordCharset != null) ? new Charset[] {passwordCharset} : new Charset[0]; 266 keystorePasswords = 267 passwordRetriever.getPasswords(keystorePasswordSpec, 268 "Keystore password for " + name, additionalPasswordEncodings); 269 loadKeyStoreFromFile( 270 ks, "NONE".equals(keystoreFile) ? null : keystoreFile, keystorePasswords); 271 } 272 273 // 3. Load the PrivateKey and cert chain from KeyStore 274 String keyAlias = null; 275 PrivateKey key = null; 276 try { 277 if (keystoreKeyAlias == null) { 278 // Private key entry alias not specified. Find the key entry contained in this 279 // KeyStore. If the KeyStore contains multiple key entries, return an error. 280 Enumeration<String> aliases = ks.aliases(); 281 if (aliases != null) { 282 while (aliases.hasMoreElements()) { 283 String entryAlias = aliases.nextElement(); 284 if (ks.isKeyEntry(entryAlias)) { 285 keyAlias = entryAlias; 286 if (keystoreKeyAlias != null) { 287 throw new ParameterException( 288 keystoreFile 289 + " contains multiple key entries" 290 + ". --ks-key-alias option must be used to specify" 291 + " which entry to use."); 292 } 293 keystoreKeyAlias = keyAlias; 294 } 295 } 296 } 297 if (keystoreKeyAlias == null) { 298 throw new ParameterException(keystoreFile + " does not contain key entries"); 299 } 300 } 301 302 // Private key entry alias known. Load that entry's private key. 303 keyAlias = keystoreKeyAlias; 304 if (!ks.isKeyEntry(keyAlias)) { 305 throw new ParameterException( 306 keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); 307 } 308 309 Key entryKey; 310 if (keyPasswordSpec != null) { 311 // Key password spec is explicitly specified. Use this spec to obtain the 312 // password and then load the key using that password. 313 List<char[]> keyPasswords = 314 passwordRetriever.getPasswords( 315 keyPasswordSpec, 316 "Key \"" + keyAlias + "\" password for " + name, 317 additionalPasswordEncodings); 318 entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); 319 } else { 320 // Key password spec is not specified. This means we should assume that key 321 // password is the same as the keystore password and that, if this assumption is 322 // wrong, we should prompt for key password and retry loading the key using that 323 // password. 324 try { 325 entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords); 326 } catch (UnrecoverableKeyException expected) { 327 List<char[]> keyPasswords = 328 passwordRetriever.getPasswords( 329 PasswordRetriever.SPEC_STDIN, 330 "Key \"" + keyAlias + "\" password for " + name, 331 additionalPasswordEncodings); 332 entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); 333 } 334 } 335 336 if (entryKey == null) { 337 throw new ParameterException( 338 keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); 339 } else if (!(entryKey instanceof PrivateKey)) { 340 throw new ParameterException( 341 keystoreFile 342 + " entry \"" 343 + keyAlias 344 + "\" does not contain a private" 345 + " key. It contains a key of algorithm: " 346 + entryKey.getAlgorithm()); 347 } 348 key = (PrivateKey) entryKey; 349 } catch (UnrecoverableKeyException e) { 350 throw new IOException( 351 "Failed to obtain key with alias \"" 352 + keyAlias 353 + "\" from " 354 + keystoreFile 355 + ". Wrong password?", 356 e); 357 } 358 this.privateKey = key; 359 Certificate[] certChain = ks.getCertificateChain(keyAlias); 360 if ((certChain == null) || (certChain.length == 0)) { 361 throw new ParameterException( 362 keystoreFile + " entry \"" + keyAlias + "\" does not contain certificates"); 363 } 364 this.certs = new ArrayList<>(certChain.length); 365 for (Certificate cert : certChain) { 366 this.certs.add((X509Certificate) cert); 367 } 368 } 369 370 /** 371 * Loads the password-protected keystore from storage. 372 * 373 * @param file file backing the keystore or {@code null} if the keystore is not file-backed, for 374 * example, a PKCS #11 KeyStore. 375 */ loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords)376 private static void loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords) 377 throws Exception { 378 Exception lastFailure = null; 379 for (char[] password : passwords) { 380 try { 381 if (file != null) { 382 try (FileInputStream in = new FileInputStream(file)) { 383 ks.load(in, password); 384 } 385 } else { 386 ks.load(null, password); 387 } 388 return; 389 } catch (Exception e) { 390 lastFailure = e; 391 } 392 } 393 if (lastFailure == null) { 394 throw new RuntimeException("No keystore passwords"); 395 } else { 396 throw lastFailure; 397 } 398 } 399 getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords)400 private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords) 401 throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { 402 UnrecoverableKeyException lastFailure = null; 403 for (char[] password : passwords) { 404 try { 405 return ks.getKey(keyAlias, password); 406 } catch (UnrecoverableKeyException e) { 407 lastFailure = e; 408 } 409 } 410 if (lastFailure == null) { 411 throw new RuntimeException("No key passwords"); 412 } else { 413 throw lastFailure; 414 } 415 } 416 loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriever)417 private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriever) 418 throws Exception { 419 if (keyFile == null) { 420 throw new ParameterException("Private key file (--key) must be specified"); 421 } 422 if (certFile == null) { 423 throw new ParameterException("Certificate file (--cert) must be specified"); 424 } 425 byte[] privateKeyBlob = readFully(new File(keyFile)); 426 427 PKCS8EncodedKeySpec keySpec; 428 // Potentially encrypted key blob 429 try { 430 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = 431 new EncryptedPrivateKeyInfo(privateKeyBlob); 432 433 // The blob is indeed an encrypted private key blob 434 String passwordSpec = 435 (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN; 436 Charset[] additionalPasswordEncodings = 437 (passwordCharset != null) ? new Charset[] {passwordCharset} : new Charset[0]; 438 List<char[]> keyPasswords = 439 passwordRetriever.getPasswords( 440 passwordSpec, "Private key password for " + name, 441 additionalPasswordEncodings); 442 keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords); 443 } catch (IOException e) { 444 // The blob is not an encrypted private key blob 445 if (keyPasswordSpec == null) { 446 // Given that no password was specified, assume the blob is an unencrypted 447 // private key blob 448 keySpec = new PKCS8EncodedKeySpec(privateKeyBlob); 449 } else { 450 throw new InvalidKeySpecException( 451 "Failed to parse encrypted private key blob " + keyFile, e); 452 } 453 } 454 455 // Load the private key from its PKCS #8 encoded form. 456 try { 457 privateKey = loadPkcs8EncodedPrivateKey(keySpec); 458 } catch (InvalidKeySpecException e) { 459 throw new InvalidKeySpecException( 460 "Failed to load PKCS #8 encoded private key from " + keyFile, e); 461 } 462 463 // Load certificates 464 Collection<? extends Certificate> certs; 465 try (FileInputStream in = new FileInputStream(certFile)) { 466 certs = X509CertificateUtils.generateCertificates(in); 467 } 468 List<X509Certificate> certList = new ArrayList<>(certs.size()); 469 for (Certificate cert : certs) { 470 certList.add((X509Certificate) cert); 471 } 472 this.certs = certList; 473 } 474 decryptPkcs8EncodedKey( EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords)475 private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey( 476 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords) 477 throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { 478 SecretKeyFactory keyFactory = 479 SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); 480 InvalidKeySpecException lastKeySpecException = null; 481 InvalidKeyException lastKeyException = null; 482 for (char[] password : passwords) { 483 PBEKeySpec decryptionKeySpec = new PBEKeySpec(password); 484 try { 485 SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec); 486 return encryptedPrivateKeyInfo.getKeySpec(decryptionKey); 487 } catch (InvalidKeySpecException e) { 488 lastKeySpecException = e; 489 } catch (InvalidKeyException e) { 490 lastKeyException = e; 491 } 492 } 493 if ((lastKeyException == null) && (lastKeySpecException == null)) { 494 throw new RuntimeException("No passwords"); 495 } else if (lastKeyException != null) { 496 throw lastKeyException; 497 } else { 498 throw lastKeySpecException; 499 } 500 } 501 loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec)502 private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec) 503 throws InvalidKeySpecException, NoSuchAlgorithmException { 504 try { 505 return KeyFactory.getInstance("RSA").generatePrivate(spec); 506 } catch (InvalidKeySpecException expected) { 507 } 508 try { 509 return KeyFactory.getInstance("EC").generatePrivate(spec); 510 } catch (InvalidKeySpecException expected) { 511 } 512 try { 513 return KeyFactory.getInstance("DSA").generatePrivate(spec); 514 } catch (InvalidKeySpecException expected) { 515 } 516 throw new InvalidKeySpecException("Not an RSA, EC, or DSA private key"); 517 } 518 readFully(File file)519 private static byte[] readFully(File file) throws IOException { 520 ByteArrayOutputStream result = new ByteArrayOutputStream(); 521 try (FileInputStream in = new FileInputStream(file)) { 522 drain(in, result); 523 } 524 return result.toByteArray(); 525 } 526 drain(InputStream in, OutputStream out)527 private static void drain(InputStream in, OutputStream out) throws IOException { 528 byte[] buf = new byte[65536]; 529 int chunkSize; 530 while ((chunkSize = in.read(buf)) != -1) { 531 out.write(buf, 0, chunkSize); 532 } 533 } 534 } 535