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