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    
032    package org.apache.commons.ssl;
033    
034    import org.apache.commons.ssl.asn1.ASN1EncodableVector;
035    import org.apache.commons.ssl.asn1.DERInteger;
036    import org.apache.commons.ssl.asn1.DERSequence;
037    import org.apache.commons.ssl.util.Hex;
038    
039    import java.io.ByteArrayInputStream;
040    import java.io.File;
041    import java.io.FileInputStream;
042    import java.io.FileOutputStream;
043    import java.io.IOException;
044    import java.math.BigInteger;
045    import java.security.GeneralSecurityException;
046    import java.security.InvalidKeyException;
047    import java.security.Key;
048    import java.security.KeyStore;
049    import java.security.KeyStoreException;
050    import java.security.NoSuchAlgorithmException;
051    import java.security.NoSuchProviderException;
052    import java.security.PrivateKey;
053    import java.security.PublicKey;
054    import java.security.UnrecoverableKeyException;
055    import java.security.cert.Certificate;
056    import java.security.cert.CertificateException;
057    import java.security.cert.CertificateFactory;
058    import java.security.cert.X509Certificate;
059    import java.security.interfaces.DSAParams;
060    import java.security.interfaces.DSAPrivateKey;
061    import java.security.interfaces.RSAPrivateCrtKey;
062    import java.security.interfaces.RSAPublicKey;
063    import java.util.Arrays;
064    import java.util.Collection;
065    import java.util.Collections;
066    import java.util.Enumeration;
067    import java.util.Iterator;
068    import java.util.LinkedList;
069    import 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     */
081    public 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    }