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