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