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