001/*
002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.9/src/java/org/apache/commons/ssl/Certificates.java $
003 * $Revision: 121 $
004 * $Date: 2007-11-13 21:26:57 -0800 (Tue, 13 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 java.io.BufferedInputStream;
035import java.io.BufferedOutputStream;
036import java.io.File;
037import java.io.FileInputStream;
038import java.io.FileOutputStream;
039import java.io.IOException;
040import java.io.InputStream;
041import java.io.OutputStream;
042import java.io.Serializable;
043import java.io.UnsupportedEncodingException;
044import java.math.BigInteger;
045import java.net.URL;
046import java.security.MessageDigest;
047import java.security.NoSuchAlgorithmException;
048import java.security.cert.CRL;
049import java.security.cert.CRLException;
050import java.security.cert.Certificate;
051import java.security.cert.CertificateEncodingException;
052import java.security.cert.CertificateException;
053import java.security.cert.CertificateFactory;
054import java.security.cert.CertificateParsingException;
055import java.security.cert.X509Certificate;
056import java.security.cert.X509Extension;
057import java.text.DateFormat;
058import java.text.SimpleDateFormat;
059import java.util.Arrays;
060import java.util.Collection;
061import java.util.Comparator;
062import java.util.Date;
063import java.util.HashMap;
064import java.util.HashSet;
065import java.util.Iterator;
066import java.util.LinkedList;
067import java.util.List;
068import java.util.Set;
069import java.util.StringTokenizer;
070
071/**
072 * @author Credit Union Central of British Columbia
073 * @author <a href="http://www.cucbc.com/">www.cucbc.com</a>
074 * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a>
075 * @since 19-Aug-2005
076 */
077public class Certificates {
078
079    public final static CertificateFactory CF;
080    public final static String LINE_ENDING = System.getProperty("line.separator");
081
082    private final static HashMap crl_cache = new HashMap();
083
084    public final static String CRL_EXTENSION = "2.5.29.31";
085    public final static String OCSP_EXTENSION = "1.3.6.1.5.5.7.1.1";
086    private final static DateFormat DF = new SimpleDateFormat("yyyy/MMM/dd");
087
088    public interface SerializableComparator extends Comparator, Serializable {
089    }
090
091    public final static SerializableComparator COMPARE_BY_EXPIRY =
092        new SerializableComparator() {
093            public int compare(Object o1, Object o2) {
094                X509Certificate c1 = (X509Certificate) o1;
095                X509Certificate c2 = (X509Certificate) o2;
096                if (c1 == c2) // this deals with case where both are null
097                {
098                    return 0;
099                }
100                if (c1 == null)  // non-null is always bigger than null
101                {
102                    return -1;
103                }
104                if (c2 == null) {
105                    return 1;
106                }
107                if (c1.equals(c2)) {
108                    return 0;
109                }
110                Date d1 = c1.getNotAfter();
111                Date d2 = c2.getNotAfter();
112                int c = d1.compareTo(d2);
113                if (c == 0) {
114                    String s1 = JavaImpl.getSubjectX500(c1);
115                    String s2 = JavaImpl.getSubjectX500(c2);
116                    c = s1.compareTo(s2);
117                    if (c == 0) {
118                        s1 = JavaImpl.getIssuerX500(c1);
119                        s2 = JavaImpl.getIssuerX500(c2);
120                        c = s1.compareTo(s2);
121                        if (c == 0) {
122                            BigInteger big1 = c1.getSerialNumber();
123                            BigInteger big2 = c2.getSerialNumber();
124                            c = big1.compareTo(big2);
125                            if (c == 0) {
126                                try {
127                                    byte[] b1 = c1.getEncoded();
128                                    byte[] b2 = c2.getEncoded();
129                                    int len1 = b1.length;
130                                    int len2 = b2.length;
131                                    int i = 0;
132                                    for (; i < len1 && i < len2; i++) {
133                                        c = ((int) b1[i]) - ((int) b2[i]);
134                                        if (c != 0) {
135                                            break;
136                                        }
137                                    }
138                                    if (c == 0) {
139                                        c = b1.length - b2.length;
140                                    }
141                                }
142                                catch (CertificateEncodingException cee) {
143                                    // I give up.  They can be equal if they
144                                    // really want to be this badly.
145                                    c = 0;
146                                }
147                            }
148                        }
149                    }
150                }
151                return c;
152            }
153        };
154
155    static {
156        CertificateFactory cf = null;
157        try {
158            cf = CertificateFactory.getInstance("X.509");
159        }
160        catch (CertificateException ce) {
161            ce.printStackTrace(System.out);
162        }
163        finally {
164            CF = cf;
165        }
166    }
167
168    public static String toPEMString(X509Certificate cert)
169        throws CertificateEncodingException {
170        return toString(cert.getEncoded());
171    }
172
173    public static String toString(byte[] x509Encoded) {
174        byte[] encoded = Base64.encodeBase64(x509Encoded);
175        StringBuffer buf = new StringBuffer(encoded.length + 100);
176        buf.append("-----BEGIN CERTIFICATE-----\n");
177        for (int i = 0; i < encoded.length; i += 64) {
178            if (encoded.length - i >= 64) {
179                buf.append(new String(encoded, i, 64));
180            } else {
181                buf.append(new String(encoded, i, encoded.length - i));
182            }
183            buf.append(LINE_ENDING);
184        }
185        buf.append("-----END CERTIFICATE-----");
186        buf.append(LINE_ENDING);
187        return buf.toString();
188    }
189
190    public static String toString(X509Certificate cert) {
191        return toString(cert, false);
192    }
193
194    public static String toString(X509Certificate cert, boolean htmlStyle) {
195        String cn = getCN(cert);
196        String startStart = DF.format(cert.getNotBefore());
197        String endDate = DF.format(cert.getNotAfter());
198        String subject = JavaImpl.getSubjectX500(cert);
199        String issuer = JavaImpl.getIssuerX500(cert);
200        Iterator crls = getCRLs(cert).iterator();
201        if (subject.equals(issuer)) {
202            issuer = "self-signed";
203        }
204        StringBuffer buf = new StringBuffer(128);
205        if (htmlStyle) {
206            buf.append("<strong class=\"cn\">");
207        }
208        buf.append(cn);
209        if (htmlStyle) {
210            buf.append("</strong>");
211        }
212        buf.append(LINE_ENDING);
213        buf.append("Valid: ");
214        buf.append(startStart);
215        buf.append(" - ");
216        buf.append(endDate);
217        buf.append(LINE_ENDING);
218        buf.append("s: ");
219        buf.append(subject);
220        buf.append(LINE_ENDING);
221        buf.append("i: ");
222        buf.append(issuer);
223        while (crls.hasNext()) {
224            buf.append(LINE_ENDING);
225            buf.append("CRL: ");
226            buf.append((String) crls.next());
227        }
228        buf.append(LINE_ENDING);
229        return buf.toString();
230    }
231
232    public static List getCRLs(X509Extension cert) {
233        // What follows is a poor man's CRL extractor, for those lacking
234        // a BouncyCastle "bcprov.jar" in their classpath.
235
236        // It's a very basic state-machine:  look for a standard URL scheme
237        // (such as http), and then start looking for a terminator.  After
238        // running hexdump a few times on these things, it looks to me like
239        // the UTF-8 value "65533" seems to happen near where these things
240        // terminate.  (Of course this stuff is ASN.1 and not UTF-8, but
241        // I happen to like some of the functions available to the String
242        // object).    - juliusdavies@cucbc.com, May 10th, 2006
243        byte[] bytes = cert.getExtensionValue(CRL_EXTENSION);
244        LinkedList httpCRLS = new LinkedList();
245        LinkedList ftpCRLS = new LinkedList();
246        LinkedList otherCRLS = new LinkedList();
247        if (bytes == null) {
248            // just return empty list
249            return httpCRLS;
250        } else {
251            String s;
252            try {
253                s = new String(bytes, "UTF-8");
254            }
255            catch (UnsupportedEncodingException uee) {
256                // We're screwed if this thing has more than one CRL, because
257                // the "indeOf( (char) 65533 )" below isn't going to work.
258                s = new String(bytes);
259            }
260            int pos = 0;
261            while (pos >= 0) {
262                int x = -1, y;
263                int[] indexes = new int[4];
264                indexes[0] = s.indexOf("http", pos);
265                indexes[1] = s.indexOf("ldap", pos);
266                indexes[2] = s.indexOf("file", pos);
267                indexes[3] = s.indexOf("ftp", pos);
268                Arrays.sort(indexes);
269                for (int i = 0; i < indexes.length; i++) {
270                    if (indexes[i] >= 0) {
271                        x = indexes[i];
272                        break;
273                    }
274                }
275                if (x >= 0) {
276                    y = s.indexOf((char) 65533, x);
277                    String crl = y > x ? s.substring(x, y - 1) : s.substring(x);
278                    if (y > x && crl.endsWith("0")) {
279                        crl = crl.substring(0, crl.length() - 1);
280                    }
281                    String crlTest = crl.trim().toLowerCase();
282                    if (crlTest.startsWith("http")) {
283                        httpCRLS.add(crl);
284                    } else if (crlTest.startsWith("ftp")) {
285                        ftpCRLS.add(crl);
286                    } else {
287                        otherCRLS.add(crl);
288                    }
289                    pos = y;
290                } else {
291                    pos = -1;
292                }
293            }
294        }
295
296        httpCRLS.addAll(ftpCRLS);
297        httpCRLS.addAll(otherCRLS);
298        return httpCRLS;
299    }
300
301    public static void checkCRL(X509Certificate cert)
302        throws CertificateException {
303        // String name = cert.getSubjectX500Principal().toString();
304        byte[] bytes = cert.getExtensionValue("2.5.29.31");
305        if (bytes == null) {
306            // log.warn( "Cert doesn't contain X509v3 CRL Distribution Points (2.5.29.31): " + name );
307        } else {
308            List crlList = getCRLs(cert);
309            Iterator it = crlList.iterator();
310            while (it.hasNext()) {
311                String url = (String) it.next();
312                CRLHolder holder = (CRLHolder) crl_cache.get(url);
313                if (holder == null) {
314                    holder = new CRLHolder(url);
315                    crl_cache.put(url, holder);
316                }
317                // success == false means we couldn't actually load the CRL
318                // (probably due to an IOException), so let's try the next one in
319                // our list.
320                boolean success = holder.checkCRL(cert);
321                if (success) {
322                    break;
323                }
324            }
325        }
326
327    }
328
329    public static BigInteger getFingerprint(X509Certificate x509)
330        throws CertificateEncodingException {
331        return getFingerprint(x509.getEncoded());
332    }
333
334    public static BigInteger getFingerprint(byte[] x509)
335        throws CertificateEncodingException {
336        MessageDigest sha1;
337        try {
338            sha1 = MessageDigest.getInstance("SHA1");
339        }
340        catch (NoSuchAlgorithmException nsae) {
341            throw JavaImpl.newRuntimeException(nsae);
342        }
343
344        sha1.reset();
345        byte[] result = sha1.digest(x509);
346        return new BigInteger(result);
347    }
348
349    private static class CRLHolder {
350        private final String urlString;
351
352        private File tempCRLFile;
353        private long creationTime;
354        private Set passedTest = new HashSet();
355        private Set failedTest = new HashSet();
356
357        CRLHolder(String urlString) {
358            if (urlString == null) {
359                throw new NullPointerException("urlString can't be null");
360            }
361            this.urlString = urlString;
362        }
363
364        public synchronized boolean checkCRL(X509Certificate cert)
365            throws CertificateException {
366            CRL crl = null;
367            long now = System.currentTimeMillis();
368            if (now - creationTime > 24 * 60 * 60 * 1000) {
369                // Expire cache every 24 hours
370                if (tempCRLFile != null && tempCRLFile.exists()) {
371                    tempCRLFile.delete();
372                }
373                tempCRLFile = null;
374                passedTest.clear();
375
376                /*
377                      Note:  if any certificate ever fails the check, we will
378                      remember that fact.
379
380                      This breaks with temporary "holds" that CRL's can issue.
381                      Apparently a certificate can have a temporary "hold" on its
382                      validity, but I'm not interested in supporting that.  If a "held"
383                      certificate is suddenly "unheld", you're just going to need
384                      to restart your JVM.
385                    */
386                // failedTest.clear();  <-- DO NOT UNCOMMENT!
387            }
388
389            BigInteger fingerprint = getFingerprint(cert);
390            if (failedTest.contains(fingerprint)) {
391                throw new CertificateException("Revoked by CRL (cached response)");
392            }
393            if (passedTest.contains(fingerprint)) {
394                return true;
395            }
396
397            if (tempCRLFile == null) {
398                try {
399                    // log.info( "Trying to load CRL [" + urlString + "]" );
400                    URL url = new URL(urlString);
401                    File tempFile = File.createTempFile("crl", ".tmp");
402                    tempFile.deleteOnExit();
403
404                    OutputStream out = new FileOutputStream(tempFile);
405                    out = new BufferedOutputStream(out);
406                    InputStream in = new BufferedInputStream(url.openStream());
407                    try {
408                        Util.pipeStream(in, out);
409                    }
410                    catch (IOException ioe) {
411                        // better luck next time
412                        tempFile.delete();
413                        throw ioe;
414                    }
415                    this.tempCRLFile = tempFile;
416                    this.creationTime = System.currentTimeMillis();
417                }
418                catch (IOException ioe) {
419                    // log.warn( "Cannot check CRL: " + e );
420                }
421            }
422
423            if (tempCRLFile != null && tempCRLFile.exists()) {
424                try {
425                    InputStream in = new FileInputStream(tempCRLFile);
426                    in = new BufferedInputStream(in);
427                    synchronized (CF) {
428                        crl = CF.generateCRL(in);
429                    }
430                    in.close();
431                    if (crl.isRevoked(cert)) {
432                        // log.warn( "Revoked by CRL [" + urlString + "]: " + name );
433                        passedTest.remove(fingerprint);
434                        failedTest.add(fingerprint);
435                        throw new CertificateException("Revoked by CRL");
436                    } else {
437                        passedTest.add(fingerprint);
438                    }
439                }
440                catch (IOException ioe) {
441                    // couldn't load CRL that's supposed to be stored in Temp file.
442                    // log.warn(  );
443                }
444                catch (CRLException crle) {
445                    // something is wrong with the CRL
446                    // log.warn(  );
447                }
448            }
449            return crl != null;
450        }
451    }
452
453    public static String getCN(X509Certificate cert) {
454        String[] cns = getCNs(cert);
455        boolean foundSomeCNs = cns != null && cns.length >= 1;
456        return foundSomeCNs ? cns[0] : null;
457    }
458
459    public static String[] getCNs(X509Certificate cert) {
460        LinkedList cnList = new LinkedList();
461        /*
462          Sebastian Hauer's original StrictSSLProtocolSocketFactory used
463          getName() and had the following comment:
464
465             Parses a X.500 distinguished name for the value of the
466             "Common Name" field.  This is done a bit sloppy right
467             now and should probably be done a bit more according to
468             <code>RFC 2253</code>.
469
470           I've noticed that toString() seems to do a better job than
471           getName() on these X500Principal objects, so I'm hoping that
472           addresses Sebastian's concern.
473
474           For example, getName() gives me this:
475           1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
476
477           whereas toString() gives me this:
478           EMAILADDRESS=juliusdavies@cucbc.com
479
480           Looks like toString() even works with non-ascii domain names!
481           I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
482          */
483        String subjectPrincipal = cert.getSubjectX500Principal().toString();
484        StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
485        while (st.hasMoreTokens()) {
486            String tok = st.nextToken();
487            int x = tok.indexOf("CN=");
488            if (x >= 0) {
489                cnList.add(tok.substring(x + 3));
490            }
491        }
492        if (!cnList.isEmpty()) {
493            String[] cns = new String[cnList.size()];
494            cnList.toArray(cns);
495            return cns;
496        } else {
497            return null;
498        }
499    }
500
501
502    /**
503     * Extracts the array of SubjectAlt DNS names from an X509Certificate.
504     * Returns null if there aren't any.
505     * <p/>
506     * Note:  Java doesn't appear able to extract international characters
507     * from the SubjectAlts.  It can only extract international characters
508     * from the CN field.
509     * <p/>
510     * (Or maybe the version of OpenSSL I'm using to test isn't storing the
511     * international characters correctly in the SubjectAlts?).
512     *
513     * @param cert X509Certificate
514     * @return Array of SubjectALT DNS names stored in the certificate.
515     */
516    public static String[] getDNSSubjectAlts(X509Certificate cert) {
517        LinkedList subjectAltList = new LinkedList();
518        Collection c = null;
519        try {
520            c = cert.getSubjectAlternativeNames();
521        }
522        catch (CertificateParsingException cpe) {
523            // Should probably log.debug() this?
524            cpe.printStackTrace();
525        }
526        if (c != null) {
527            Iterator it = c.iterator();
528            while (it.hasNext()) {
529                List list = (List) it.next();
530                int type = ((Integer) list.get(0)).intValue();
531                // If type is 2, then we've got a dNSName
532                if (type == 2) {
533                    String s = (String) list.get(1);
534                    subjectAltList.add(s);
535                }
536            }
537        }
538        if (!subjectAltList.isEmpty()) {
539            String[] subjectAlts = new String[subjectAltList.size()];
540            subjectAltList.toArray(subjectAlts);
541            return subjectAlts;
542        } else {
543            return null;
544        }
545    }
546
547    /**
548     * Trims off any null entries on the array.  Returns a shrunk array.
549     *
550     * @param chain X509Certificate[] chain to trim
551     * @return Shrunk array with all trailing null entries removed.
552     */
553    public static X509Certificate[] trimChain(X509Certificate[] chain) {
554        for (int i = 0; i < chain.length; i++) {
555            if (chain[i] == null) {
556                X509Certificate[] newChain = new X509Certificate[i];
557                System.arraycopy(chain, 0, newChain, 0, i);
558                return newChain;
559            }
560        }
561        return chain;
562    }
563
564    /**
565     * Returns a chain of type X509Certificate[].
566     *
567     * @param chain Certificate[] chain to cast to X509Certificate[]
568     * @return chain of type X509Certificate[].
569     */
570    public static X509Certificate[] x509ifyChain(Certificate[] chain) {
571        if (chain instanceof X509Certificate[]) {
572            return (X509Certificate[]) chain;
573        } else {
574            X509Certificate[] x509Chain = new X509Certificate[chain.length];
575            System.arraycopy(chain, 0, x509Chain, 0, chain.length);
576            return x509Chain;
577        }
578    }
579
580    public static void main(String[] args) throws Exception {
581        for (int i = 0; i < args.length; i++) {
582            FileInputStream in = new FileInputStream(args[i]);
583            TrustMaterial tm = new TrustMaterial(in);
584            Iterator it = tm.getCertificates().iterator();
585            while (it.hasNext()) {
586                X509Certificate x509 = (X509Certificate) it.next();
587                System.out.println(toString(x509));
588            }
589        }
590    }
591}