001/*
002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.9/src/java/org/apache/commons/ssl/KeyStoreBuilder.java $
003 * $Revision: 129 $
004 * $Date: 2007-11-14 19:21:33 -0800 (Wed, 14 Nov 2007) $
005 *
006 * ====================================================================
007 * Licensed to the Apache Software Foundation (ASF) under one
008 * or more contributor license agreements.  See the NOTICE file
009 * distributed with this work for additional information
010 * regarding copyright ownership.  The ASF licenses this file
011 * to you under the Apache License, Version 2.0 (the
012 * "License"); you may not use this file except in compliance
013 * with the License.  You may obtain a copy of the License at
014 *
015 *   http://www.apache.org/licenses/LICENSE-2.0
016 *
017 * Unless required by applicable law or agreed to in writing,
018 * software distributed under the License is distributed on an
019 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
020 * KIND, either express or implied.  See the License for the
021 * specific language governing permissions and limitations
022 * under the License.
023 * ====================================================================
024 *
025 * This software consists of voluntary contributions made by many
026 * individuals on behalf of the Apache Software Foundation.  For more
027 * information on the Apache Software Foundation, please see
028 * <http://www.apache.org/>.
029 *
030 */
031
032package org.apache.commons.ssl;
033
034import org.apache.commons.ssl.asn1.ASN1EncodableVector;
035import org.apache.commons.ssl.asn1.DERInteger;
036import org.apache.commons.ssl.asn1.DERSequence;
037import org.apache.commons.ssl.util.Hex;
038
039import java.io.ByteArrayInputStream;
040import java.io.File;
041import java.io.FileInputStream;
042import java.io.FileOutputStream;
043import java.io.IOException;
044import java.math.BigInteger;
045import java.security.GeneralSecurityException;
046import java.security.InvalidKeyException;
047import java.security.Key;
048import java.security.KeyStore;
049import java.security.KeyStoreException;
050import java.security.NoSuchAlgorithmException;
051import java.security.NoSuchProviderException;
052import java.security.PrivateKey;
053import java.security.PublicKey;
054import java.security.UnrecoverableKeyException;
055import java.security.cert.Certificate;
056import java.security.cert.CertificateException;
057import java.security.cert.CertificateFactory;
058import java.security.cert.X509Certificate;
059import java.security.interfaces.DSAParams;
060import java.security.interfaces.DSAPrivateKey;
061import java.security.interfaces.RSAPrivateCrtKey;
062import java.security.interfaces.RSAPublicKey;
063import java.util.Arrays;
064import java.util.Collection;
065import java.util.Collections;
066import java.util.Enumeration;
067import java.util.Iterator;
068import java.util.LinkedList;
069import java.util.List;
070
071/**
072 * Builds Java Key Store files out of pkcs12 files, or out of pkcs8 files +
073 * certificate chains.  Also supports OpenSSL style private keys (encrypted or
074 * unencrypted).
075 *
076 * @author Credit Union Central of British Columbia
077 * @author <a href="http://www.cucbc.com/">www.cucbc.com</a>
078 * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a>
079 * @since 4-Nov-2006
080 */
081public class KeyStoreBuilder {
082    private final static String PKCS7_ENCRYPTED = "1.2.840.113549.1.7.6";
083
084    public static KeyStore build(byte[] jksOrCerts, char[] password)
085        throws IOException, CertificateException, KeyStoreException,
086        NoSuchAlgorithmException, InvalidKeyException,
087        NoSuchProviderException, ProbablyBadPasswordException,
088        UnrecoverableKeyException {
089        return build(jksOrCerts, null, password);
090    }
091
092    public static KeyStore build(byte[] jksOrCerts, byte[] privateKey,
093                                 char[] password)
094        throws IOException, CertificateException, KeyStoreException,
095        NoSuchAlgorithmException, InvalidKeyException,
096        NoSuchProviderException, ProbablyBadPasswordException,
097        UnrecoverableKeyException {
098        BuildResult br1 = parse(jksOrCerts, password);
099        BuildResult br2 = null;
100        KeyStore jks = null;
101        if (br1.jks != null) {
102            jks = br1.jks;
103        } else if (privateKey != null && privateKey.length > 0) {
104            br2 = parse(privateKey, password);
105            if (br2.jks != null) {
106                jks = br2.jks;
107            }
108        }
109
110        // If we happened to find a JKS file, let's just return that.
111        // JKS files get priority (in case some weirdo specifies both a PKCS12
112        // and a JKS file!).
113        if (jks != null) {
114            // Make sure the keystore we found is not corrupt.
115            validate(jks, password);
116            return jks;
117        }
118
119        Key key = br1.key;
120        X509Certificate[] chain = br1.chain;
121        boolean atLeastOneNotSet = key == null || chain == null;
122        if (atLeastOneNotSet && br2 != null) {
123            if (br2.key != null) {
124                // Notice that the key from build-result-2 gets priority over the
125                // key from build-result-1 (if both had valid keys).
126                key = br2.key;
127            }
128            if (chain == null) {
129                chain = br2.chain;
130            }
131        }
132
133        atLeastOneNotSet = key == null || chain == null;
134        if (atLeastOneNotSet) {
135            String missing = "";
136            if (key == null) {
137                missing = " [Private key missing (bad password?)]";
138            }
139            if (chain == null) {
140                missing += " [Certificate chain missing]";
141            }
142            throw new KeyStoreException("Can't build keystore:" + missing);
143        } else {
144
145            X509Certificate theOne = buildChain(key, chain);
146            String alias = "alias";
147            // The theOne is not null, then our chain was probably altered.
148            // Need to trim out the newly introduced null entries at the end of
149            // our chain.
150            if (theOne != null) {
151                chain = Certificates.trimChain(chain);
152                alias = Certificates.getCN(theOne);
153                alias = alias.replace(' ', '_');
154            }
155
156            KeyStore ks = KeyStore.getInstance("jks");
157            ks.load(null, password);
158            ks.setKeyEntry(alias, key, password, chain);
159            return ks;
160        }
161    }
162
163    /**
164     * Builds the chain up such that chain[ 0 ] contains the public key
165     * corresponding to the supplied private key.
166     *
167     * @param key   private key
168     * @param chain array of certificates to build chain from
169     * @return theOne!
170     * @throws KeyStoreException        no certificates correspond to private key
171     * @throws CertificateException     java libraries complaining
172     * @throws NoSuchAlgorithmException java libraries complaining
173     * @throws InvalidKeyException      java libraries complaining
174     * @throws NoSuchProviderException  java libraries complaining
175     */
176    public static X509Certificate buildChain(Key key, Certificate[] chain)
177        throws CertificateException, KeyStoreException,
178        NoSuchAlgorithmException, InvalidKeyException,
179        NoSuchProviderException {
180        X509Certificate theOne = null;
181        if (key instanceof RSAPrivateCrtKey) {
182            final RSAPrivateCrtKey rsa = (RSAPrivateCrtKey) key;
183            BigInteger publicExponent = rsa.getPublicExponent();
184            BigInteger modulus = rsa.getModulus();
185            for (int i = 0; i < chain.length; i++) {
186                X509Certificate c = (X509Certificate) chain[i];
187                PublicKey pub = c.getPublicKey();
188                if (pub instanceof RSAPublicKey) {
189                    RSAPublicKey certKey = (RSAPublicKey) pub;
190                    BigInteger pe = certKey.getPublicExponent();
191                    BigInteger mod = certKey.getModulus();
192                    if (publicExponent.equals(pe) && modulus.equals(mod)) {
193                        theOne = c;
194                    }
195                }
196            }
197            if (theOne == null) {
198                throw new KeyStoreException("Can't build keystore: [No certificates belong to the private-key]");
199            }
200            X509Certificate[] newChain;
201            newChain = X509CertificateChainBuilder.buildPath(theOne, chain);
202            Arrays.fill(chain, null);
203            System.arraycopy(newChain, 0, chain, 0, newChain.length);
204        }
205        return theOne;
206    }
207
208    public static void validate(KeyStore jks, char[] password)
209        throws CertificateException, KeyStoreException,
210        NoSuchAlgorithmException, InvalidKeyException,
211        NoSuchProviderException, UnrecoverableKeyException {
212        Enumeration en = jks.aliases();
213        String privateKeyAlias = null;
214        while (en.hasMoreElements()) {
215            String alias = (String) en.nextElement();
216            boolean isKey = jks.isKeyEntry(alias);
217            if (isKey) {
218                if (privateKeyAlias != null) {
219                    throw new KeyStoreException("Only 1 private key per keystore allowed for Commons-SSL");
220                } else {
221                    privateKeyAlias = alias;
222                }
223            }
224        }
225        if (privateKeyAlias == null) {
226            throw new KeyStoreException("No private keys found in keystore!");
227        }
228        PrivateKey key = (PrivateKey) jks.getKey(privateKeyAlias, password);
229        Certificate[] chain = jks.getCertificateChain(privateKeyAlias);
230        X509Certificate[] x509Chain = Certificates.x509ifyChain(chain);
231        X509Certificate theOne = buildChain(key, x509Chain);
232        // The theOne is not null, then our chain was probably altered.
233        // Need to trim out the newly introduced null entries at the end of
234        // our chain.
235        if (theOne != null) {
236            x509Chain = Certificates.trimChain(x509Chain);
237            jks.deleteEntry(privateKeyAlias);
238            jks.setKeyEntry(privateKeyAlias, key, password, x509Chain);
239        }
240    }
241
242    protected static class BuildResult {
243        protected final Key key;
244        protected final X509Certificate[] chain;
245        protected final KeyStore jks;
246
247        protected BuildResult(Key key, Certificate[] chain, KeyStore jks) {
248            this.key = key;
249            this.jks = jks;
250            if (chain == null) {
251                this.chain = null;
252            } else if (chain instanceof X509Certificate[]) {
253                this.chain = (X509Certificate[]) chain;
254            } else {
255                X509Certificate[] x509 = new X509Certificate[chain.length];
256                System.arraycopy(chain, 0, x509, 0, chain.length);
257                this.chain = x509;
258            }
259        }
260    }
261
262
263    public static BuildResult parse(byte[] stuff, char[] password)
264        throws IOException, CertificateException, KeyStoreException,
265        ProbablyBadPasswordException {
266        CertificateFactory cf = CertificateFactory.getInstance("X.509");
267        Key key = null;
268        Certificate[] chain = null;
269        try {
270            PKCS8Key pkcs8Key = new PKCS8Key(stuff, password);
271            key = pkcs8Key.getPrivateKey();
272        }
273        catch (ProbablyBadPasswordException pbpe) {
274            throw pbpe;
275        }
276        catch (GeneralSecurityException gse) {
277            // no luck
278        }
279
280        List pemItems = PEMUtil.decode(stuff);
281        Iterator it = pemItems.iterator();
282        LinkedList certificates = new LinkedList();
283        while (it.hasNext()) {
284            PEMItem item = (PEMItem) it.next();
285            byte[] derBytes = item.getDerBytes();
286            String type = item.pemType.trim().toUpperCase();
287            if (type.startsWith("CERT") ||
288                type.startsWith("X509") ||
289                type.startsWith("PKCS7")) {
290                ByteArrayInputStream in = new ByteArrayInputStream(derBytes);
291                X509Certificate c = (X509Certificate) cf.generateCertificate(in);
292                certificates.add(c);
293            }
294            chain = toChain(certificates);
295        }
296
297        if (chain != null || key != null) {
298            return new BuildResult(key, chain, null);
299        }
300
301        boolean isProbablyPKCS12 = false;
302        boolean isASN = false;
303        boolean isProbablyJKS = stuff.length >= 4 &&
304                                stuff[0] == (byte) 0xFE &&
305                                stuff[1] == (byte) 0xED &&
306                                stuff[2] == (byte) 0xFE &&
307                                stuff[3] == (byte) 0xED;
308
309        ASN1Structure asn1 = null;
310        try {
311            asn1 = ASN1Util.analyze(stuff);
312            isASN = true;
313            isProbablyPKCS12 = asn1.oids.contains(PKCS7_ENCRYPTED);
314            if (!isProbablyPKCS12 && asn1.bigPayload != null) {
315                asn1 = ASN1Util.analyze(asn1.bigPayload);
316                isProbablyPKCS12 = asn1.oids.contains(PKCS7_ENCRYPTED);
317            }
318        }
319        catch (Exception e) {
320            // isProbablyPKCS12 and isASN are set properly by now.
321        }
322
323        ByteArrayInputStream stuffStream = new ByteArrayInputStream(stuff);
324        if (isProbablyJKS) {
325            try {
326                return tryJKS("jks", stuffStream, password);
327            }
328            catch (ProbablyBadPasswordException pbpe) {
329                throw pbpe;
330            }
331            catch (GeneralSecurityException gse) {
332                // jks didn't work.
333            }
334            catch (IOException ioe) {
335                // jks didn't work.
336            }
337        }
338        if (isASN) {
339            if (isProbablyPKCS12) {
340                try {
341                    return tryJKS("pkcs12", stuffStream, password);
342                }
343                catch (ProbablyBadPasswordException pbpe) {
344                    throw pbpe;
345                }
346                catch (GeneralSecurityException gse) {
347                    // pkcs12 didn't work.
348                }
349                catch (IOException ioe) {
350                    // pkcs12 didn't work.
351                }
352            } else {
353                // Okay, it's ASN.1, but it's not PKCS12.  Only one possible
354                // interesting things remains:  X.509.
355                stuffStream.reset();
356
357                try {
358                    certificates = new LinkedList();
359                    Collection certs = cf.generateCertificates(stuffStream);
360                    it = certs.iterator();
361                    while (it.hasNext()) {
362                        X509Certificate x509 = (X509Certificate) it.next();
363                        certificates.add(x509);
364                    }
365                    chain = toChain(certificates);
366                    if (chain != null && chain.length > 0) {
367                        return new BuildResult(null, chain, null);
368                    }
369                }
370                catch (CertificateException ce) {
371                    // oh well
372                }
373
374                stuffStream.reset();
375                // Okay, still no luck.  Maybe it's an ASN.1 DER stream
376                // containing only a single certificate?  (I don't completely
377                // trust CertificateFactory.generateCertificates).
378                try {
379                    Certificate c = cf.generateCertificate(stuffStream);
380                    X509Certificate x509 = (X509Certificate) c;
381                    chain = toChain(Collections.singleton(x509));
382                    if (chain != null && chain.length > 0) {
383                        return new BuildResult(null, chain, null);
384                    }
385                }
386                catch (CertificateException ce) {
387                    // oh well
388                }
389            }
390        }
391
392        if (!isProbablyJKS) {
393            String hex = Hex.encode(stuff, 0, 4);
394            try {
395                BuildResult br = tryJKS("jks", stuffStream, password);
396                // no exception thrown, so must be JKS.
397                System.out.println("Please report bug!");
398                System.out.println("JKS usually start with binary FE ED FE ED, but this JKS started with: [" + hex + "]");
399                return br;
400            }
401            catch (ProbablyBadPasswordException pbpe) {
402                System.out.println("Please report bug!");
403                System.out.println("JKS usually start with binary FE ED FE ED, but this JKS started with: [" + hex + "]");
404                throw pbpe;
405            }
406            catch (GeneralSecurityException gse) {
407                // jks didn't work.
408            }
409            catch (IOException ioe) {
410                // jks didn't work.
411            }
412        }
413
414        if (!isProbablyPKCS12) {
415            try {
416                BuildResult br = tryJKS("pkcs12", stuffStream, password);
417                // no exception thrown, so must be PKCS12.
418                System.out.println("Please report bug!");
419                System.out.println("PKCS12 detection failed to realize this was PKCS12!");
420                System.out.println(asn1);
421                return br;
422            }
423            catch (ProbablyBadPasswordException pbpe) {
424                System.out.println("Please report bug!");
425                System.out.println("PKCS12 detection failed to realize this was PKCS12!");
426                System.out.println(asn1);
427                throw pbpe;
428            }
429            catch (GeneralSecurityException gse) {
430                // pkcs12 didn't work.
431            }
432            catch (IOException ioe) {
433                // pkcs12 didn't work.
434            }
435        }
436        throw new KeyStoreException("failed to extract any certificates or private keys - maybe bad password?");
437    }
438
439    private static BuildResult tryJKS(String keystoreType,
440                                      ByteArrayInputStream in,
441                                      char[] password)
442        throws GeneralSecurityException, IOException {
443        in.reset();
444        keystoreType = keystoreType.trim().toLowerCase();
445        boolean isPKCS12 = "pkcs12".equals(keystoreType);
446        KeyStore jksKeyStore = KeyStore.getInstance(keystoreType);
447        try {
448            Key key = null;
449            Certificate[] chain = null;
450            jksKeyStore.load(in, password);
451            Enumeration en = jksKeyStore.aliases();
452            while (en.hasMoreElements()) {
453                String alias = (String) en.nextElement();
454                if (jksKeyStore.isKeyEntry(alias)) {
455                    key = jksKeyStore.getKey(alias, password);
456                    if (key != null && key instanceof PrivateKey) {
457                        chain = jksKeyStore.getCertificateChain(alias);
458                        break;
459                    }
460                }
461                if (isPKCS12 && en.hasMoreElements()) {
462                    System.out.println("what kind of weird pkcs12 file has more than one alias?");
463                }
464            }
465            if (isPKCS12) {
466                // PKCS12 is supposed to be just a key and a chain, anyway.
467                jksKeyStore = null;
468            }
469            return new BuildResult(key, chain, jksKeyStore);
470        }
471        catch (GeneralSecurityException gse) {
472            throw gse;
473        }
474        catch (IOException ioe) {
475            ioe.printStackTrace();
476
477            String msg = ioe.getMessage();
478            msg = msg != null ? msg.trim().toLowerCase() : "";
479            if (isPKCS12) {
480                int x = msg.indexOf("failed to decrypt");
481                int y = msg.indexOf("verify mac");
482                x = Math.max(x, y);
483                if (x >= 0) {
484                    throw new ProbablyBadPasswordException("Probably bad PKCS12 password: " + ioe);
485                }
486            } else {
487                int x = msg.indexOf("password");
488                if (x >= 0) {
489                    throw new ProbablyBadPasswordException("Probably bad JKS password: " + ioe);
490                }
491            }
492            ioe.printStackTrace();
493            throw ioe;
494        }
495    }
496
497    private static X509Certificate[] toChain(Collection certs) {
498        if (certs != null && !certs.isEmpty()) {
499            X509Certificate[] x509Chain = new X509Certificate[certs.size()];
500            certs.toArray(x509Chain);
501            return x509Chain;
502        } else {
503            return null;
504        }
505    }
506
507
508    public static void main(String[] args) throws Exception {
509        if (args.length < 2) {
510            System.out.println("KeyStoreBuilder:  creates '[alias].jks' (Java Key Store)");
511            System.out.println("    -topk8 mode:  creates '[alias].pem' (x509 chain + unencrypted pkcs8)");
512            System.out.println("[alias] will be set to the first CN value of the X509 certificate.");
513            System.out.println("-------------------------------------------------------------------");
514            System.out.println("Usage1: [password] [file:pkcs12]");
515            System.out.println("Usage2: [password] [file:private-key] [file:certificate-chain]");
516            System.out.println("Usage3: -topk8 [password] [file:jks]");
517            System.out.println("-------------------------------------------------------------------");
518            System.out.println("[private-key] can be openssl format, or pkcs8.");
519            System.out.println("[password] decrypts [private-key], and also encrypts outputted JKS file.");
520            System.out.println("All files can be PEM or DER.");
521            System.exit(1);
522        }
523        char[] password = args[0].toCharArray();
524        boolean toPKCS8 = false;
525        if ("-topk8".equalsIgnoreCase(args[0])) {
526            toPKCS8 = true;
527            password = args[1].toCharArray();
528            args[1] = args[2];
529            args[2] = null;
530        }
531
532        FileInputStream fin1 = new FileInputStream(args[1]);
533        byte[] bytes1 = Util.streamToBytes(fin1);
534        byte[] bytes2 = null;
535        if (args.length > 2 && args[2] != null) {
536            FileInputStream fin2 = new FileInputStream(args[2]);
537            bytes2 = Util.streamToBytes(fin2);
538        }
539
540        KeyStore ks = build(bytes1, bytes2, password);
541        Enumeration en = ks.aliases();
542        String alias = null;
543        while (en.hasMoreElements()) {
544            if (alias == null) {
545                alias = (String) en.nextElement();
546            } else {
547                System.out.println("Generated keystore contains more than 1 alias!?!?");
548            }
549        }
550
551        String suffix = toPKCS8 ? ".pem" : ".jks";
552        File f = new File(alias + suffix);
553        int count = 1;
554        while (f.exists()) {
555            f = new File(alias + "_" + count + suffix);
556            count++;
557        }
558
559        FileOutputStream jks = new FileOutputStream(f);
560        if (toPKCS8) {
561            List pemItems = new LinkedList();
562            PrivateKey key = (PrivateKey) ks.getKey(alias, password);
563            Certificate[] chain = ks.getCertificateChain(alias);
564            byte[] pkcs8DerBytes = null;
565            if (key instanceof RSAPrivateCrtKey) {
566                RSAPrivateCrtKey rsa = (RSAPrivateCrtKey) key;
567                ASN1EncodableVector vec = new ASN1EncodableVector();
568                vec.add(new DERInteger(BigInteger.ZERO));
569                vec.add(new DERInteger(rsa.getModulus()));
570                vec.add(new DERInteger(rsa.getPublicExponent()));
571                vec.add(new DERInteger(rsa.getPrivateExponent()));
572                vec.add(new DERInteger(rsa.getPrimeP()));
573                vec.add(new DERInteger(rsa.getPrimeQ()));
574                vec.add(new DERInteger(rsa.getPrimeExponentP()));
575                vec.add(new DERInteger(rsa.getPrimeExponentQ()));
576                vec.add(new DERInteger(rsa.getCrtCoefficient()));
577                DERSequence seq = new DERSequence(vec);
578                byte[] derBytes = PKCS8Key.encode(seq);
579                PKCS8Key pkcs8 = new PKCS8Key(derBytes, null);
580                pkcs8DerBytes = pkcs8.getDecryptedBytes();
581            } else if (key instanceof DSAPrivateKey) {
582                DSAPrivateKey dsa = (DSAPrivateKey) key;
583                DSAParams params = dsa.getParams();
584                BigInteger g = params.getG();
585                BigInteger p = params.getP();
586                BigInteger q = params.getQ();
587                BigInteger x = dsa.getX();
588                BigInteger y = q.modPow(x, p);
589
590                ASN1EncodableVector vec = new ASN1EncodableVector();
591                vec.add(new DERInteger(BigInteger.ZERO));
592                vec.add(new DERInteger(p));
593                vec.add(new DERInteger(q));
594                vec.add(new DERInteger(g));
595                vec.add(new DERInteger(y));
596                vec.add(new DERInteger(x));
597                DERSequence seq = new DERSequence(vec);
598                byte[] derBytes = PKCS8Key.encode(seq);
599                PKCS8Key pkcs8 = new PKCS8Key(derBytes, null);
600                pkcs8DerBytes = pkcs8.getDecryptedBytes();
601            }
602            if (chain != null && chain.length > 0) {
603                for (int i = 0; i < chain.length; i++) {
604                    X509Certificate x509 = (X509Certificate) chain[i];
605                    byte[] derBytes = x509.getEncoded();
606                    PEMItem item = new PEMItem(derBytes, "CERTIFICATE");
607                    pemItems.add(item);
608                }
609            }
610            if (pkcs8DerBytes != null) {
611                PEMItem item = new PEMItem(pkcs8DerBytes, "PRIVATE KEY");
612                pemItems.add(item);
613            }
614            byte[] pem = PEMUtil.encode(pemItems);
615            jks.write(pem);
616        } else {
617            ks.store(jks, password);
618        }
619        jks.flush();
620        jks.close();
621        System.out.println("Successfuly wrote: [" + f.getPath() + "]");
622    }
623
624
625}