Bypass testing for a private key/public key match if keying data isn't compatible...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / utils / ExtKeyTool.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu>Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.utils;
27
28 import java.io.BufferedInputStream;
29 import java.io.BufferedReader;
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.PrintStream;
39 import java.security.Key;
40 import java.security.KeyFactory;
41 import java.security.KeyStore;
42 import java.security.KeyStoreException;
43 import java.security.NoSuchAlgorithmException;
44 import java.security.NoSuchProviderException;
45 import java.security.PrivateKey;
46 import java.security.Provider;
47 import java.security.PublicKey;
48 import java.security.Security;
49 import java.security.UnrecoverableKeyException;
50 import java.security.cert.CertificateException;
51 import java.security.cert.CertificateFactory;
52 import java.security.cert.X509Certificate;
53 import java.security.interfaces.RSAKey;
54 import java.security.spec.PKCS8EncodedKeySpec;
55 import java.util.ArrayList;
56 import java.util.Collection;
57 import java.util.Properties;
58
59 import javax.crypto.Cipher;
60
61 import org.apache.log4j.ConsoleAppender;
62 import org.apache.log4j.Level;
63 import org.apache.log4j.LogManager;
64 import org.apache.log4j.Logger;
65 import org.apache.log4j.PatternLayout;
66 import sun.misc.BASE64Encoder;
67
68 /**
69  * Extension utility for use alongside Sun's keytool program. Performs useful functions not found in original.
70  * 
71  * @author Walter Hoehn
72  */
73
74 public class ExtKeyTool {
75
76         protected static Logger log = Logger.getLogger(ExtKeyTool.class.getName());
77
78         /**
79          * Creates and initializes a java <code>KeyStore</code>
80          * 
81          * @param provider
82          *            name of the jce provider to use in loading the keystore
83          * @param keyStoreStream
84          *            stream used to retrieve the keystore
85          * @param storeType
86          *            the type of the keystore
87          * @param keyStorePassword
88          *            password used to verify the integrity of the keystore
89          * @throws ExtKeyToolException
90          *             if a problem is encountered loading the keystore
91          */
92
93         protected KeyStore loadKeyStore(String provider, InputStream keyStoreStream, String storeType,
94                         char[] keyStorePassword) throws ExtKeyToolException {
95
96                 try {
97                         if (storeType == null) {
98                                 storeType = "JKS";
99                         }
100
101                         log.debug("Using keystore type: (" + storeType + ")");
102                         log.debug("Using provider: (" + provider + ")");
103
104                         KeyStore keyStore;
105                         if (storeType.equals("JKS")) {
106                                 keyStore = KeyStore.getInstance(storeType, "SUN");
107                         } else if (storeType.equals("JCEKS")) {
108                                 keyStore = KeyStore.getInstance(storeType, "SunJCE");
109                         } else {
110                                 keyStore = KeyStore.getInstance(storeType, provider);
111                         }
112
113                         if (keyStoreStream == null) {
114                                 log.error("Keystore must be specified.");
115                                 throw new ExtKeyToolException("Keystore must be specified.");
116                         }
117                         if (keyStorePassword == null) {
118                                 log.warn("No password given for keystore, integrity will not be verified.");
119                         }
120                         keyStore.load(keyStoreStream, keyStorePassword);
121
122                         return keyStore;
123
124                 } catch (KeyStoreException e) {
125                         log.error("Problem loading keystore: " + e);
126                         throw new ExtKeyToolException("Problem loading keystore: " + e);
127                 } catch (NoSuchProviderException e) {
128                         log.error("The specified provider is not available.");
129                         throw new ExtKeyToolException("The specified provider is not available.");
130                 } catch (CertificateException ce) {
131                         log.error("Could not open keystore: " + ce);
132                         throw new ExtKeyToolException("Could not open keystore: " + ce);
133                 } catch (IOException ioe) {
134                         log.error("Could not export key: " + ioe);
135                         throw new ExtKeyToolException("Could not export key: " + ioe);
136                 } catch (NoSuchAlgorithmException nse) {
137                         log.error("Could not open keystore with the installed JCE providers: " + nse);
138                         throw new ExtKeyToolException("Could not open keystore with the installed JCE providers: " + nse);
139                 }
140         }
141
142         /**
143          * Retrieves a private key from a java keystore and writes it to an <code>PrintStream</code>
144          * 
145          * @param provider
146          *            name of the jce provider to use in retrieving the key
147          * @param outStream
148          *            stream that should be used to output the retrieved key
149          * @param keyStoreStream
150          *            stream used to retrieve the keystore
151          * @param storeType
152          *            the type of the keystore
153          * @param keyStorePassword
154          *            password used to verify the integrity of the keystore
155          * @param keyAlias
156          *            the alias under which the key is stored
157          * @param keyPassword
158          *            the password for recovering the key
159          * @param rfc
160          *            boolean indicator of whether the key should be Base64 encoded before being written to the stream
161          * @throws ExtKeyToolException
162          *             if there a problem retrieving or writing the key
163          */
164
165         public void exportKey(String provider, PrintStream outStream, InputStream keyStoreStream, String storeType,
166                         char[] keyStorePassword, String keyAlias, char[] keyPassword, boolean rfc) throws ExtKeyToolException {
167
168                 try {
169
170                         KeyStore keyStore = loadKeyStore(provider, keyStoreStream, storeType, keyStorePassword);
171
172                         if (keyAlias == null) {
173                                 log.error("Key alias must be specified.");
174                                 throw new ExtKeyToolException("Key alias must be specified.");
175                         }
176                         log.info("Searching for key.");
177
178                         Key key = keyStore.getKey(keyAlias, keyPassword);
179                         if (key == null) {
180                                 log.error("Key not found in store.");
181                                 throw new ExtKeyToolException("Key not found in store.");
182                         }
183                         log.info("Found key.");
184
185                         if (rfc) {
186                                 log.debug("Dumping with rfc encoding");
187                                 outStream.println("-----BEGIN PRIVATE KEY-----");
188                                 BASE64Encoder encoder = new BASE64Encoder();
189                                 encoder.encodeBuffer(key.getEncoded(), outStream);
190                                 outStream.println("-----END PRIVATE KEY-----");
191                         } else {
192                                 log.debug("Dumping with default encoding.");
193                                 outStream.write(key.getEncoded());
194                         }
195
196                 } catch (KeyStoreException e) {
197                         log.error("Problem accessing keystore: " + e);
198                         throw new ExtKeyToolException("Problem loading keystore: " + e);
199                 } catch (IOException ioe) {
200                         log.error("Could not export key: " + ioe);
201                         throw new ExtKeyToolException("Could not export key: " + ioe);
202                 } catch (NoSuchAlgorithmException nse) {
203                         log.error("Could not recover key with the installed JCE providers: " + nse);
204                         throw new ExtKeyToolException("Could not recover key with the installed JCE providers: " + nse);
205                 } catch (UnrecoverableKeyException uke) {
206                         log.error("The key specified key cannot be recovered with the given password: " + uke);
207                         throw new ExtKeyToolException("The key specified key cannot be recovered with the given password: " + uke);
208                 }
209         }
210
211         /**
212          * Boolean indication of whether a given private key and public key form a valid keypair.
213          * 
214          * @param pubKey
215          *            the public key
216          * @param privKey
217          *            the private key
218          */
219
220         protected boolean isMatchingKey(String algorithm, PublicKey pubKey, PrivateKey privKey) {
221
222                 try {
223                         String controlString = "asdf";
224                         log.debug("Checking for matching private key/public key pair");
225
226                         /*
227                          * If both keys are RSA, compare the modulus. They can't be a pair if that doesn't match. Doing this early
228                          * check means we don't have to do a trial encryption for every public key (faster) and avoids warning
229                          * messages from the depths of the crypto provider if the key lengths differ.
230                          */
231                         if (privKey instanceof RSAKey && pubKey instanceof RSAKey) {
232                                 RSAKey pubRSA = (RSAKey) pubKey;
233                                 RSAKey privRSA = (RSAKey) privKey;
234                                 if (!privRSA.getModulus().equals(pubRSA.getModulus())) {
235                                         log.debug("RSA modulus mismatch");
236                                         return false;
237                                 }
238                         }
239
240                         Cipher cipher = Cipher.getInstance(algorithm);
241                         cipher.init(Cipher.ENCRYPT_MODE, pubKey);
242                         byte[] encryptedData = cipher.doFinal(controlString.getBytes("UTF-8"));
243
244                         cipher.init(Cipher.DECRYPT_MODE, privKey);
245                         byte[] decryptedData = cipher.doFinal(encryptedData);
246                         if (controlString.equals(new String(decryptedData, "UTF-8"))) {
247                                 log.debug("Found match.");
248                                 return true;
249                         }
250                 } catch (Exception e) {
251                         log.warn(e);
252                 }
253                 log.debug("This pair does not match.");
254                 return false;
255         }
256
257         /**
258          * Attempts to unmarshall a private key from a given stream.
259          * 
260          * @param keyStream
261          *            the <code>InputStream</code> suppying the key
262          * @param algorithm
263          *            the key algorithm
264          * @throws ExtKeyToolException
265          *             if there a problem unmarshalling the key
266          */
267
268         protected PrivateKey readPrivateKey(String provider, InputStream keyStream, String algorithm)
269                         throws ExtKeyToolException {
270
271                 try {
272                         KeyFactory keyFactory = KeyFactory.getInstance(algorithm, provider);
273
274                         byte[] inputBuffer = new byte[8];
275                         int i;
276                         ByteContainer inputBytes = new ByteContainer(400);
277                         do {
278                                 i = keyStream.read(inputBuffer);
279                                 for (int j = 0; j < i; j++) {
280                                         inputBytes.append(inputBuffer[j]);
281                                 }
282                         } while (i > -1);
283
284                         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(inputBytes.toByteArray());
285                         return keyFactory.generatePrivate(keySpec);
286
287                 } catch (Exception e) {
288                         log.error("Problem reading private key: " + e.getMessage());
289                         throw new ExtKeyToolException(
290                                         "Problem reading private key.  Keys should be DER encoded pkcs8 or DER encoded native format.");
291                 }
292         }
293
294         /**
295          * Converts an array of certificates into an ordered chain. A certificate that matches the specified private key
296          * will be returned first and the root certificate will be returned last.
297          * 
298          * @param untestedCerts
299          *            array of certificates
300          * @param privKey
301          *            the private key used to determine the first cert in the chain
302          * @throws InvalidCertificateChainException
303          *             thrown if a chain cannot be constructed from the specified elements
304          */
305
306         protected X509Certificate[] linkChain(String keyAlgorithm, X509Certificate[] untestedCerts, PrivateKey privKey)
307                         throws InvalidCertificateChainException {
308
309                 log.debug("Located " + untestedCerts.length + " cert(s) in input file");
310
311                 log.info("Finding end cert in chain.");
312                 ArrayList replyCerts = new ArrayList();
313                 for (int i = 0; untestedCerts.length > i; i++) {
314                         if (isMatchingKey(keyAlgorithm, untestedCerts[i].getPublicKey(), privKey)) {
315                                 log.debug("Found matching end cert: " + untestedCerts[i].getSubjectDN());
316                                 replyCerts.add(untestedCerts[i]);
317                         }
318                 }
319                 if (replyCerts.size() < 1) {
320                         log.error("No certificate in chain that matches specified private key");
321                         throw new InvalidCertificateChainException("No certificate in chain that matches specified private key");
322                 }
323                 if (replyCerts.size() > 1) {
324                         log.error("More than one certificate in chain that matches specified private key");
325                         throw new InvalidCertificateChainException(
326                                         "More than one certificate in chain that matches specified private key");
327                 }
328
329                 log.info("Populating chain with remaining certs.");
330                 walkChain(untestedCerts, replyCerts);
331
332                 log.info("Verifying that each link in the cert chain is signed appropriately");
333                 for (int i = 0; i < replyCerts.size() - 1; i++) {
334                         PublicKey pubKey = ((X509Certificate) replyCerts.get(i + 1)).getPublicKey();
335                         try {
336                                 ((X509Certificate) replyCerts.get(i)).verify(pubKey);
337                         } catch (Exception e) {
338                                 log.error("Certificate chain cannot be verified: " + e.getMessage());
339                                 throw new InvalidCertificateChainException("Certificate chain cannot be verified: " + e.getMessage());
340                         }
341                 }
342                 log.info("All signatures verified. Certificate chain successfully created.");
343
344                 return (X509Certificate[]) replyCerts.toArray(new X509Certificate[0]);
345         }
346
347         /**
348          * Given an ArrayList containing a base certificate and an array of unordered certificates, populates the ArrayList
349          * with an ordered certificate chain, based on subject and issuer.
350          * 
351          * @param chainSource
352          *            array of certificates to pull from
353          * @param chainDest
354          *            ArrayList containing base certificate
355          * @throws InvalidCertificateChainException
356          *             thrown if a chain cannot be constructed from the specified elements
357          */
358
359         protected void walkChain(X509Certificate[] chainSource, ArrayList chainDest)
360                         throws InvalidCertificateChainException {
361
362                 X509Certificate currentCert = (X509Certificate) chainDest.get(chainDest.size() - 1);
363                 if (currentCert.getSubjectDN().equals(currentCert.getIssuerDN())) {
364                         log.debug("Found self-signed root cert: " + currentCert.getSubjectDN());
365                         return;
366                 } else {
367                         for (int i = 0; chainSource.length > i; i++) {
368                                 if (currentCert.getIssuerDN().equals(chainSource[i].getSubjectDN())) {
369                                         chainDest.add(chainSource[i]);
370                                         walkChain(chainSource, chainDest);
371                                         return;
372                                 }
373                         }
374                         log.error("Incomplete certificate chain.");
375                         throw new InvalidCertificateChainException("Incomplete cerficate chain.");
376                 }
377         }
378
379         /**
380          * Given a java keystore, private key, and matching certificate chain; creates a new keystore containing the union
381          * of these objects
382          * 
383          * @param provider
384          *            the name of the jce provider to use
385          * @param keyAlgorithm
386          *            the algorithm of the key to be added, defaults to RSA if null
387          * @param keyStream
388          *            strema used to retrieve the private key, can contain a PEM encoded or pkcs8 encoded key
389          * @param chainStream
390          *            stream used to retrieve certificates, can contain a series of PEM encoded certs or a pkcs7 chain
391          * @param keyStoreInStream
392          *            stream used to retrieve the initial keystore
393          * @param storeType
394          *            the type of the keystore
395          * @param keyAlias
396          *            the alias under which the key/chain should be saved
397          * @param keyStorePassword
398          *            password used to verify the integrity of the old keystore and save the new keystore
399          * @param keyPassword
400          *            the password for saving the key
401          * @return an OutputStream containing the new keystore
402          * @throws ExtKeyToolException
403          *             if there a problem importing the key
404          */
405
406         public ByteArrayOutputStream importKey(String provider, String keyAlgorithm, InputStream keyStream,
407                         InputStream chainStream, InputStream keyStoreInStream, String storeType, String keyAlias,
408                         char[] keyStorePassword, char[] keyPassword) throws ExtKeyToolException {
409
410                 log.info("Importing key pair.");
411                 try {
412
413                         // The Sun provider incorrectly reads only the first cert in the stream.
414                         // No loss, it won't even read RSA keys
415                         if (provider == "SUN") {
416                                 log.error("Sorry, this function not supported with the SUN provider.");
417                                 throw new ExtKeyToolException("Sorry, this function not supported with the SUN provider.");
418                         }
419
420                         KeyStore keyStore = loadKeyStore(provider, keyStoreInStream, storeType, keyStorePassword);
421
422                         if (keyAlias == null) {
423                                 log.error("Key alias must be specified.");
424                                 throw new ExtKeyToolException("Key alias must be specified.");
425                         }
426                         if (keyStore.containsAlias(keyAlias) == true && keyStore.isKeyEntry(keyAlias)) {
427                                 log.error("Could not import key: " + "key alias (" + keyAlias + ") already exists");
428                                 throw new ExtKeyToolException("Could not import key: " + "key alias (" + keyAlias + ") already exists");
429                         }
430                         keyStore.deleteEntry(keyAlias);
431
432                         log.info("Reading private key.");
433                         if (keyAlgorithm == null) {
434                                 keyAlgorithm = "RSA";
435                         }
436                         log.debug("Using key algorithm: (" + keyAlgorithm + ")");
437                         PrivateKey key = readPrivateKey(provider, keyStream, keyAlgorithm);
438
439                         log.info("Reading certificate chain.");
440
441                         CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider);
442                         Collection chain = certFactory.generateCertificates(new BufferedInputStream(chainStream));
443                         if (chain.isEmpty()) {
444                                 log.error("Input did not contain any valid certificates.");
445                                 throw new ExtKeyToolException("Input did not contain any valid certificates.");
446                         }
447
448                         X509Certificate[] verifiedChain = linkChain(keyAlgorithm, (X509Certificate[]) chain
449                                         .toArray(new X509Certificate[0]), key);
450
451                         keyStore.setKeyEntry(keyAlias, key, keyPassword, verifiedChain);
452                         ByteArrayOutputStream keyStoreOutStream = new ByteArrayOutputStream();
453                         keyStore.store(keyStoreOutStream, keyStorePassword);
454                         log.info("Key Store saved to stream.");
455                         return keyStoreOutStream;
456
457                 } catch (KeyStoreException e) {
458                         log.error("Encountered a problem accessing the keystore: " + e.getMessage());
459                         throw new ExtKeyToolException("Encountered a problem accessing the keystore: " + e.getMessage());
460                 } catch (CertificateException e) {
461                         log.error("Could not load certificate(s): " + e.getMessage());
462                         throw new ExtKeyToolException("Could not load certificate(s): " + e.getMessage());
463                 } catch (NoSuchProviderException e) {
464                         log.error("The specified provider is not available.");
465                         throw new ExtKeyToolException("The specified provider is not available.");
466                 } catch (IOException ioe) {
467                         log.error("Could not export key: " + ioe);
468                         throw new ExtKeyToolException("Could not export key: " + ioe);
469                 } catch (NoSuchAlgorithmException nse) {
470                         log.error("Could not save with the installed JCE providers: " + nse);
471                         throw new ExtKeyToolException("Could not save with the installed JCE providers: " + nse);
472                 }
473         }
474
475         /**
476          * Tries to decipher command line arguments.
477          * 
478          * @throws IllegalArgumentException
479          *             if arguments are not properly formatted
480          */
481
482         private static Properties parseArguments(String[] args) throws IllegalArgumentException {
483
484                 if (args.length < 1) { throw new IllegalArgumentException("No arguments found."); }
485                 Properties parsedArguments = new Properties();
486
487                 for (int i = 0; (i < args.length) && args[i].startsWith("-"); i++) {
488
489                         String flags = args[i];
490
491                         //parse actions
492                         if (flags.equalsIgnoreCase("-exportkey")) {
493                                 parsedArguments.setProperty("command", "exportKey");
494                         } else if (flags.equalsIgnoreCase("-importkey")) {
495                                 parsedArguments.setProperty("command", "importKey");
496                         }
497
498                         //parse specifiers
499                         else if (flags.equalsIgnoreCase("-alias")) {
500                                 if (++i == args.length) { throw new IllegalArgumentException("The argument -alias requires a parameter"); }
501                                 parsedArguments.setProperty("alias", args[i]);
502                         } else if (flags.equalsIgnoreCase("-keyfile")) {
503                                 if (++i == args.length) { throw new IllegalArgumentException(
504                                                 "The argument -keyfile requires a parameter"); }
505                                 parsedArguments.setProperty("keyFile", args[i]);
506                         } else if (flags.equalsIgnoreCase("-certfile")) {
507                                 if (++i == args.length) { throw new IllegalArgumentException(
508                                                 "The argument -certfile requires a parameter"); }
509                                 parsedArguments.setProperty("certFile", args[i]);
510                         } else if (flags.equalsIgnoreCase("-keystore")) {
511                                 if (++i == args.length) { throw new IllegalArgumentException(
512                                                 "The argument -keystore requires a parameter"); }
513                                 parsedArguments.setProperty("keyStore", args[i]);
514                         } else if (flags.equalsIgnoreCase("-storepass")) {
515                                 if (++i == args.length) { throw new IllegalArgumentException(
516                                                 "The argument -storepass requires a parameter"); }
517                                 parsedArguments.setProperty("storePass", args[i]);
518                         } else if (flags.equalsIgnoreCase("-storetype")) {
519                                 if (++i == args.length) { throw new IllegalArgumentException(
520                                                 "The argument -storetype requires a parameter"); }
521                                 parsedArguments.setProperty("storeType", args[i]);
522                         } else if (flags.equalsIgnoreCase("-keypass")) {
523                                 if (++i == args.length) { throw new IllegalArgumentException(
524                                                 "The argument -keypass requires a parameter"); }
525                                 parsedArguments.setProperty("keyPass", args[i]);
526                         } else if (flags.equalsIgnoreCase("-provider")) {
527                                 if (++i == args.length) { throw new IllegalArgumentException(
528                                                 "The argument -provider requires a parameter"); }
529                                 parsedArguments.setProperty("provider", args[i]);
530                         } else if (flags.equalsIgnoreCase("-file")) {
531                                 if (++i == args.length) { throw new IllegalArgumentException("The argument -file requires a parameter"); }
532                                 parsedArguments.setProperty("file", args[i]);
533                         } else if (flags.equalsIgnoreCase("-algorithm")) {
534                                 if (++i == args.length) { throw new IllegalArgumentException(
535                                                 "The argument -algorithm requires a parameter"); }
536                                 parsedArguments.setProperty("keyAlgorithm", args[i]);
537                         }
538
539                         //options
540                         else if (flags.equalsIgnoreCase("-v")) {
541                                 parsedArguments.setProperty("verbose", "true");
542                         } else if (flags.equalsIgnoreCase("-rfc")) {
543                                 parsedArguments.setProperty("rfc", "true");
544                         } else {
545                                 throw new IllegalArgumentException("Unrecognized argument: " + flags);
546                         }
547                 }
548                 if (parsedArguments.getProperty("command", null) == null) { throw new IllegalArgumentException(
549                                 "No action specified"); }
550                 return parsedArguments;
551         }
552
553         /**
554          * Ensures that providers specified on the command line are in fact loaded into the current environment.
555          * 
556          * @return the name of the provider add, null if no provider was added
557          */
558
559         protected String initProvider(Properties arguments) {
560
561                 try {
562                         if (arguments.getProperty("provider", null) != null) {
563
564                                 Provider provider = (Provider) Class.forName(arguments.getProperty("provider")).newInstance();
565                                 log.info("Adding Provider to environment: (" + provider.getName() + ")");
566                                 Security.addProvider(provider);
567                                 return provider.getName();
568                         }
569                 } catch (Exception e) {
570                         log.error("Could not load specified jce provider: " + e);
571                 }
572                 return null;
573
574         }
575
576         /**
577          * Initializes Log4J logger mode based on command line arguments.
578          */
579
580         protected void startLogger(Properties arguments) {
581
582                 Logger root = Logger.getRootLogger();
583                 if (arguments.getProperty("verbose", null) == null || arguments.getProperty("verbose", null).equals("false")) {
584                         root.addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.DEFAULT_CONVERSION_PATTERN)));
585                         root.setLevel(Level.WARN);
586                 } else {
587                         root.addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
588                         root.setLevel(Level.DEBUG);
589                 }
590         }
591
592         public static void main(String[] args) {
593
594                 try {
595                         ExtKeyTool tool = new ExtKeyTool();
596                         Properties arguments = null;
597                         try {
598                                 arguments = parseArguments(args);
599                         } catch (IllegalArgumentException iae) {
600                                 System.err.println("Illegal argument specified: " + iae.getMessage()
601                                                 + System.getProperty("line.separator"));
602                                 printUsage(System.err);
603                                 System.exit(1);
604                         }
605                         tool.startLogger(arguments);
606                         String providerName = tool.initProvider(arguments);
607                         if (providerName != null) {
608                                 arguments.setProperty("providerName", providerName);
609                         }
610                         tool.run(arguments);
611
612                 } catch (ExtKeyToolException ske) {
613                         log.fatal("Cannot Perform Operation: " + ske.getMessage() + System.getProperty("line.separator"));
614                         LogManager.shutdown();
615                         printUsage(System.err);
616                 }
617         }
618
619         /**
620          * Based on on a set of properties, executes <code>ExtKeyTool</code> actions.
621          * 
622          * @param arguments
623          *            runtime parameters specified on the command line
624          */
625
626         private void run(Properties arguments) throws ExtKeyToolException {
627
628                 //common for all actions
629                 char[] storePassword = null;
630                 if (arguments.getProperty("storePass", null) != null) {
631                         storePassword = arguments.getProperty("storePass").toCharArray();
632                 }
633
634                 String providerName = null;
635                 if (arguments.getProperty("providerName", null) != null) {
636                         providerName = arguments.getProperty("providerName");
637                 } else {
638                         providerName = "SUN";
639                 }
640
641                 //export key action
642                 if (arguments.getProperty("command").equals("exportKey")) {
643
644                         boolean rfc = false;
645                         if ("true".equalsIgnoreCase(arguments.getProperty("rfc", null))) {
646                                 rfc = true;
647                         }
648
649                         PrintStream outStream = null;
650                         if (arguments.getProperty("file", null) != null) {
651                                 try {
652                                         outStream = new PrintStream(new FileOutputStream(arguments.getProperty("file")));
653                                 } catch (FileNotFoundException e) {
654                                         throw new ExtKeyToolException("Could not open output file: " + e);
655                                 }
656                         } else {
657                                 outStream = System.out;
658                         }
659
660                         try {
661                                 exportKey(providerName, outStream, new FileInputStream(resolveKeyStore(arguments.getProperty(
662                                                 "keyStore", null))), arguments.getProperty("storeType", null), storePassword, arguments
663                                                 .getProperty("alias", null), resolveKeyPass(arguments.getProperty("keyPass", null),
664                                                 storePassword), rfc);
665                         } catch (FileNotFoundException e) {
666                                 throw new ExtKeyToolException("KeyStore not found.");
667                         }
668                         outStream.close();
669
670                         //import action
671                 } else if (arguments.getProperty("command").equals("importKey")) {
672
673                         InputStream keyInStream = null;
674                         if (arguments.getProperty("keyFile", null) != null) {
675                                 try {
676                                         keyInStream = new FileInputStream(arguments.getProperty("keyFile"));
677                                 } catch (FileNotFoundException e) {
678                                         throw new ExtKeyToolException("Could not open key file." + e.getMessage());
679                                 }
680                         } else {
681                                 throw new IllegalArgumentException("Key file must be specified.");
682                         }
683
684                         InputStream certInStream = null;
685                         if (arguments.getProperty("certFile", null) != null) {
686                                 try {
687                                         certInStream = new FileInputStream(arguments.getProperty("certFile"));
688                                 } catch (FileNotFoundException e) {
689                                         throw new ExtKeyToolException("Could not open cert file." + e.getMessage());
690                                 }
691                         } else {
692                                 throw new IllegalArgumentException("Certificate file must be specified.");
693                         }
694
695                         try {
696
697                                 ByteArrayOutputStream keyStoreOutStream = importKey(providerName, arguments.getProperty("keyAlgorithm",
698                                                 null), keyInStream, certInStream, new FileInputStream(resolveKeyStore(arguments.getProperty(
699                                                 "keyStore", null))), arguments.getProperty("storeType", null), arguments.getProperty("alias",
700                                                 null), storePassword, resolveKeyPass(arguments.getProperty("keyPass", null), storePassword));
701
702                                 keyInStream.close();
703                                 // A quick sanity check before we overwrite the old keystore
704                                 if (keyStoreOutStream == null || keyStoreOutStream.size() < 1) { throw new ExtKeyToolException(
705                                                 "Failed to create keystore: results are null"); }
706                                 keyStoreOutStream
707                                                 .writeTo(new FileOutputStream(resolveKeyStore(arguments.getProperty("keyStore", null))));
708                                 System.out.println("Key import successful.");
709
710                         } catch (FileNotFoundException e) {
711                                 throw new ExtKeyToolException("Could not open keystore file." + e.getMessage());
712                         } catch (IOException e) {
713                                 throw new ExtKeyToolException("Error writing keystore." + e.getMessage());
714                         }
715
716                 } else {
717                         throw new IllegalArgumentException("This keytool cannot perform the operation: ("
718                                         + arguments.getProperty("command") + ")");
719                 }
720
721         }
722
723         /**
724          * Determines the location of the keystore to use when performing the action
725          * 
726          * @return the <code>File</code> representation of the selected keystore
727          */
728
729         protected File resolveKeyStore(String keyStoreLocation) throws ExtKeyToolException, FileNotFoundException {
730
731                 if (keyStoreLocation == null) {
732                         keyStoreLocation = System.getProperty("user.home") + File.separator + ".keystore";
733                 }
734                 log.debug("Using keystore (" + keyStoreLocation + ")");
735                 File file = new File(keyStoreLocation);
736                 if (file.exists() && file.length() == 0) {
737                         log.error("Keystore file is empty.");
738                         throw new ExtKeyToolException("Keystore file is empty.");
739                 }
740                 return file;
741         }
742
743         /**
744          * Decides what password to use for storing/retrieving keys from the keystore. NOTE: Possible terminal interaction
745          * with the user.
746          * 
747          * @return a char array containing the password
748          */
749
750         protected char[] resolveKeyPass(String keyPass, char[] storePass) {
751
752                 if (keyPass != null) {
753                         return keyPass.toCharArray();
754                 } else {
755                         System.out.println("Enter key password");
756                         System.out.print("\t(RETURN if same as keystore password):  ");
757                         System.out.flush();
758                         try {
759                                 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
760                                 String passwordInput = reader.readLine();
761                                 passwordInput.trim();
762                                 if (passwordInput != null && !passwordInput.equals("")) { return passwordInput.toCharArray(); }
763                         } catch (IOException e) {
764                                 log.warn(e.getMessage());
765                         }
766                         log.warn("No password specified, defaulting to keystore password.");
767                         return storePass;
768                 }
769         }
770
771         private static void printUsage(PrintStream out) {
772
773                 out.println("extkeytool usage:");
774                 out.print("-exportkey      [-v] [-rfc] [-alias <alias>] ");
775                 out.println("[-keystore <keystore>] ");
776                 out.print("\t     [-storepass <storepass>] ");
777                 out.println("[-storetype <storetype>]");
778                 out.print("\t     [-keypass <keypass>] ");
779                 out.println("[-provider <provider_class_name>] ");
780                 out.print("\t     [-file <output_file>] ");
781                 out.println();
782                 out.println();
783
784                 out.print("-importkey      [-v] [-alias <alias>] ");
785                 out.println("[-keyfile <key_file>]");
786                 out.print("\t     [-keystore <keystore>] ");
787                 out.println("[-storepass <storepass>]");
788                 out.print("\t     [-storetype <storetype>] ");
789                 out.println("[-keypass <keypass>] ");
790                 out.print("\t     [-provider <provider_class_name>] ");
791                 out.println("[-certfile <cert_file>] ");
792                 out.print("\t     [-algorithm <key_algorithm>] ");
793                 out.println();
794
795         }
796
797         /**
798          * Auto-enlarging container for bytes.
799          */
800
801         // Sure makes you wish bytes were first class objects.
802         private class ByteContainer {
803
804                 private byte[] buffer;
805                 private int cushion;
806                 private int currentSize = 0;
807
808                 private ByteContainer(int cushion) {
809
810                         buffer = new byte[cushion];
811                         this.cushion = cushion;
812                 }
813
814                 private void grow() {
815
816                         log.debug("Growing ByteContainer.");
817                         int newSize = currentSize + cushion;
818                         byte[] b = new byte[newSize];
819                         int toCopy = Math.min(currentSize, newSize);
820                         int i;
821                         for (i = 0; i < toCopy; i++) {
822                                 b[i] = buffer[i];
823                         }
824                         buffer = b;
825                 }
826
827                 /**
828                  * Returns an array of the bytes in the container.
829                  * <p>
830                  */
831
832                 private byte[] toByteArray() {
833
834                         byte[] b = new byte[currentSize];
835                         for (int i = 0; i < currentSize; i++) {
836                                 b[i] = buffer[i];
837                         }
838                         return b;
839                 }
840
841                 /**
842                  * Add one byte to the end of the container.
843                  */
844
845                 private void append(byte b) {
846
847                         if (currentSize == buffer.length) {
848                                 grow();
849                         }
850                         buffer[currentSize] = b;
851                         currentSize++;
852                 }
853
854         }
855
856         /**
857          * Signals that an error was encounted while using <code>ExtKeyTool</code> functions.
858          */
859
860         protected class ExtKeyToolException extends Exception {
861
862                 protected ExtKeyToolException(String message) {
863
864                         super(message);
865                 }
866         }
867
868         /**
869          * Signals that an error occurred while trying to constuct a certificate chain.
870          */
871
872         protected class InvalidCertificateChainException extends ExtKeyToolException {
873
874                 protected InvalidCertificateChainException(String message) {
875
876                         super(message);
877                 }
878         }
879
880 }