    001/*
    002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.15/src/java/org/apache/commons/ssl/Certificates.java $
    003 * $Revision: 172 $
    004 * $Date: 2014-09-06 16:52:31 -0700 (Sat, 06 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, new Integer[]{new Integer(5000)});
    409                            setReadTimeout.invoke(httpConn, new Integer[]{new Integer(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}