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