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    
032    package org.apache.commons.ssl;
033    
034    import javax.net.ssl.HttpsURLConnection;
035    import java.io.*;
036    import java.math.BigInteger;
037    import java.net.URL;
038    import java.net.URLConnection;
039    import java.net.HttpURLConnection;
040    import java.security.MessageDigest;
041    import java.security.NoSuchAlgorithmException;
042    import java.security.cert.*;
043    import java.text.DateFormat;
044    import java.text.SimpleDateFormat;
045    import java.util.*;
046    import 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     */
054    public 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    }