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