• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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