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