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 // Single-arg Provider constructor 213 ksProvider = 214 (Provider) ksProviderClass.getConstructor(String.class) 215 .newInstance(keystoreProviderArg); 216 } else { 217 // No-arg Provider constructor 218 ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); 219 } 220 ks = KeyStore.getInstance(ksType, ksProvider); 221 } else { 222 // Use the highest-priority Provider which offers the requested KeyStore type 223 ks = KeyStore.getInstance(ksType); 224 } 225 226 // 2. Load the KeyStore 227 List<char[]> keystorePasswords; 228 Charset[] additionalPasswordEncodings; 229 { 230 String keystorePasswordSpec = 231 (this.keystorePasswordSpec != null) 232 ? this.keystorePasswordSpec 233 : PasswordRetriever.SPEC_STDIN; 234 additionalPasswordEncodings = 235 (passwordCharset != null) ? new Charset[] {passwordCharset} : new Charset[0]; 236 keystorePasswords = 237 passwordRetriever.getPasswords(keystorePasswordSpec, 238 "Keystore password for " + name, additionalPasswordEncodings); 239 loadKeyStoreFromFile( 240 ks, "NONE".equals(keystoreFile) ? null : keystoreFile, keystorePasswords); 241 } 242 243 // 3. Load the PrivateKey and cert chain from KeyStore 244 String keyAlias = null; 245 PrivateKey key = null; 246 try { 247 if (keystoreKeyAlias == null) { 248 // Private key entry alias not specified. Find the key entry contained in this 249 // KeyStore. If the KeyStore contains multiple key entries, return an error. 250 Enumeration<String> aliases = ks.aliases(); 251 if (aliases != null) { 252 while (aliases.hasMoreElements()) { 253 String entryAlias = aliases.nextElement(); 254 if (ks.isKeyEntry(entryAlias)) { 255 keyAlias = entryAlias; 256 if (keystoreKeyAlias != null) { 257 throw new ParameterException( 258 keystoreFile 259 + " contains multiple key entries" 260 + ". --ks-key-alias option must be used to specify" 261 + " which entry to use."); 262 } 263 keystoreKeyAlias = keyAlias; 264 } 265 } 266 } 267 if (keystoreKeyAlias == null) { 268 throw new ParameterException(keystoreFile + " does not contain key entries"); 269 } 270 } 271 272 // Private key entry alias known. Load that entry's private key. 273 keyAlias = keystoreKeyAlias; 274 if (!ks.isKeyEntry(keyAlias)) { 275 throw new ParameterException( 276 keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); 277 } 278 279 Key entryKey; 280 if (keyPasswordSpec != null) { 281 // Key password spec is explicitly specified. Use this spec to obtain the 282 // password and then load the key using that password. 283 List<char[]> keyPasswords = 284 passwordRetriever.getPasswords( 285 keyPasswordSpec, 286 "Key \"" + keyAlias + "\" password for " + name, 287 additionalPasswordEncodings); 288 entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); 289 } else { 290 // Key password spec is not specified. This means we should assume that key 291 // password is the same as the keystore password and that, if this assumption is 292 // wrong, we should prompt for key password and retry loading the key using that 293 // password. 294 try { 295 entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords); 296 } catch (UnrecoverableKeyException expected) { 297 List<char[]> keyPasswords = 298 passwordRetriever.getPasswords( 299 PasswordRetriever.SPEC_STDIN, 300 "Key \"" + keyAlias + "\" password for " + name, 301 additionalPasswordEncodings); 302 entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); 303 } 304 } 305 306 if (entryKey == null) { 307 throw new ParameterException( 308 keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); 309 } else if (!(entryKey instanceof PrivateKey)) { 310 throw new ParameterException( 311 keystoreFile 312 + " entry \"" 313 + keyAlias 314 + "\" does not contain a private" 315 + " key. It contains a key of algorithm: " 316 + entryKey.getAlgorithm()); 317 } 318 key = (PrivateKey) entryKey; 319 } catch (UnrecoverableKeyException e) { 320 throw new IOException( 321 "Failed to obtain key with alias \"" 322 + keyAlias 323 + "\" from " 324 + keystoreFile 325 + ". Wrong password?", 326 e); 327 } 328 this.privateKey = key; 329 Certificate[] certChain = ks.getCertificateChain(keyAlias); 330 if ((certChain == null) || (certChain.length == 0)) { 331 throw new ParameterException( 332 keystoreFile + " entry \"" + keyAlias + "\" does not contain certificates"); 333 } 334 this.certs = new ArrayList<>(certChain.length); 335 for (Certificate cert : certChain) { 336 this.certs.add((X509Certificate) cert); 337 } 338 } 339 340 /** 341 * Loads the password-protected keystore from storage. 342 * 343 * @param file file backing the keystore or {@code null} if the keystore is not file-backed, for 344 * example, a PKCS #11 KeyStore. 345 */ loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords)346 private static void loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords) 347 throws Exception { 348 Exception lastFailure = null; 349 for (char[] password : passwords) { 350 try { 351 if (file != null) { 352 try (FileInputStream in = new FileInputStream(file)) { 353 ks.load(in, password); 354 } 355 } else { 356 ks.load(null, password); 357 } 358 return; 359 } catch (Exception e) { 360 lastFailure = e; 361 } 362 } 363 if (lastFailure == null) { 364 throw new RuntimeException("No keystore passwords"); 365 } else { 366 throw lastFailure; 367 } 368 } 369 getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords)370 private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords) 371 throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { 372 UnrecoverableKeyException lastFailure = null; 373 for (char[] password : passwords) { 374 try { 375 return ks.getKey(keyAlias, password); 376 } catch (UnrecoverableKeyException e) { 377 lastFailure = e; 378 } 379 } 380 if (lastFailure == null) { 381 throw new RuntimeException("No key passwords"); 382 } else { 383 throw lastFailure; 384 } 385 } 386 loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriever)387 private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriever) 388 throws Exception { 389 if (keyFile == null) { 390 throw new ParameterException("Private key file (--key) must be specified"); 391 } 392 if (certFile == null) { 393 throw new ParameterException("Certificate file (--cert) must be specified"); 394 } 395 byte[] privateKeyBlob = readFully(new File(keyFile)); 396 397 PKCS8EncodedKeySpec keySpec; 398 // Potentially encrypted key blob 399 try { 400 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = 401 new EncryptedPrivateKeyInfo(privateKeyBlob); 402 403 // The blob is indeed an encrypted private key blob 404 String passwordSpec = 405 (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN; 406 Charset[] additionalPasswordEncodings = 407 (passwordCharset != null) ? new Charset[] {passwordCharset} : new Charset[0]; 408 List<char[]> keyPasswords = 409 passwordRetriever.getPasswords( 410 passwordSpec, "Private key password for " + name, 411 additionalPasswordEncodings); 412 keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords); 413 } catch (IOException e) { 414 // The blob is not an encrypted private key blob 415 if (keyPasswordSpec == null) { 416 // Given that no password was specified, assume the blob is an unencrypted 417 // private key blob 418 keySpec = new PKCS8EncodedKeySpec(privateKeyBlob); 419 } else { 420 throw new InvalidKeySpecException( 421 "Failed to parse encrypted private key blob " + keyFile, e); 422 } 423 } 424 425 // Load the private key from its PKCS #8 encoded form. 426 try { 427 privateKey = loadPkcs8EncodedPrivateKey(keySpec); 428 } catch (InvalidKeySpecException e) { 429 throw new InvalidKeySpecException( 430 "Failed to load PKCS #8 encoded private key from " + keyFile, e); 431 } 432 433 // Load certificates 434 Collection<? extends Certificate> certs; 435 try (FileInputStream in = new FileInputStream(certFile)) { 436 certs = X509CertificateUtils.generateCertificates(in); 437 } 438 List<X509Certificate> certList = new ArrayList<>(certs.size()); 439 for (Certificate cert : certs) { 440 certList.add((X509Certificate) cert); 441 } 442 this.certs = certList; 443 } 444 decryptPkcs8EncodedKey( EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords)445 private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey( 446 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords) 447 throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { 448 SecretKeyFactory keyFactory = 449 SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); 450 InvalidKeySpecException lastKeySpecException = null; 451 InvalidKeyException lastKeyException = null; 452 for (char[] password : passwords) { 453 PBEKeySpec decryptionKeySpec = new PBEKeySpec(password); 454 try { 455 SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec); 456 return encryptedPrivateKeyInfo.getKeySpec(decryptionKey); 457 } catch (InvalidKeySpecException e) { 458 lastKeySpecException = e; 459 } catch (InvalidKeyException e) { 460 lastKeyException = e; 461 } 462 } 463 if ((lastKeyException == null) && (lastKeySpecException == null)) { 464 throw new RuntimeException("No passwords"); 465 } else if (lastKeyException != null) { 466 throw lastKeyException; 467 } else { 468 throw lastKeySpecException; 469 } 470 } 471 loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec)472 private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec) 473 throws InvalidKeySpecException, NoSuchAlgorithmException { 474 try { 475 return KeyFactory.getInstance("RSA").generatePrivate(spec); 476 } catch (InvalidKeySpecException expected) { 477 } 478 try { 479 return KeyFactory.getInstance("EC").generatePrivate(spec); 480 } catch (InvalidKeySpecException expected) { 481 } 482 try { 483 return KeyFactory.getInstance("DSA").generatePrivate(spec); 484 } catch (InvalidKeySpecException expected) { 485 } 486 throw new InvalidKeySpecException("Not an RSA, EC, or DSA private key"); 487 } 488 readFully(File file)489 private static byte[] readFully(File file) throws IOException { 490 ByteArrayOutputStream result = new ByteArrayOutputStream(); 491 try (FileInputStream in = new FileInputStream(file)) { 492 drain(in, result); 493 } 494 return result.toByteArray(); 495 } 496 drain(InputStream in, OutputStream out)497 private static void drain(InputStream in, OutputStream out) throws IOException { 498 byte[] buf = new byte[65536]; 499 int chunkSize; 500 while ((chunkSize = in.read(buf)) != -1) { 501 out.write(buf, 0, chunkSize); 502 } 503 } 504 } 505