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