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    
032    package org.apache.commons.ssl;
033    
034    import java.io.BufferedInputStream;
035    import java.io.BufferedOutputStream;
036    import java.io.File;
037    import java.io.FileInputStream;
038    import java.io.FileOutputStream;
039    import java.io.IOException;
040    import java.io.InputStream;
041    import java.io.OutputStream;
042    import java.io.Serializable;
043    import java.io.UnsupportedEncodingException;
044    import java.math.BigInteger;
045    import java.net.URL;
046    import java.security.MessageDigest;
047    import java.security.NoSuchAlgorithmException;
048    import java.security.cert.CRL;
049    import java.security.cert.CRLException;
050    import java.security.cert.Certificate;
051    import java.security.cert.CertificateEncodingException;
052    import java.security.cert.CertificateException;
053    import java.security.cert.CertificateFactory;
054    import java.security.cert.CertificateParsingException;
055    import java.security.cert.X509Certificate;
056    import java.security.cert.X509Extension;
057    import java.text.DateFormat;
058    import java.text.SimpleDateFormat;
059    import java.util.Arrays;
060    import java.util.Collection;
061    import java.util.Comparator;
062    import java.util.Date;
063    import java.util.HashMap;
064    import java.util.HashSet;
065    import java.util.Iterator;
066    import java.util.LinkedList;
067    import java.util.List;
068    import java.util.Set;
069    import 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     */
077    public 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 "花子.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    }