001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010 Sun Microsystems, Inc.
015 * Portions copyright 2012-2016 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.security.GeneralSecurityException;
023import java.security.KeyStore;
024import java.security.NoSuchAlgorithmException;
025import java.security.cert.CertificateException;
026import java.security.cert.CertificateExpiredException;
027import java.security.cert.CertificateNotYetValidException;
028import java.security.cert.X509Certificate;
029import java.util.Date;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033import javax.net.ssl.TrustManager;
034import javax.net.ssl.TrustManagerFactory;
035import javax.net.ssl.X509TrustManager;
036
037import org.forgerock.opendj.ldap.schema.Schema;
038import org.forgerock.util.Reject;
039
040/** This class contains methods for creating common types of trust manager. */
041public final class TrustManagers {
042
043    /**
044     * An X509TrustManager which rejects certificate chains whose subject DN
045     * does not match a specified host name.
046     */
047    private static final class CheckHostName implements X509TrustManager {
048
049        private final X509TrustManager trustManager;
050
051        private final String hostName;
052
053        private CheckHostName(final X509TrustManager trustManager, final String hostName) {
054            this.trustManager = trustManager;
055            this.hostName = hostName;
056        }
057
058        @Override
059        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
060                throws CertificateException {
061            verifyHostName(chain);
062            trustManager.checkClientTrusted(chain, authType);
063        }
064
065        @Override
066        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
067                throws CertificateException {
068            verifyHostName(chain);
069            trustManager.checkServerTrusted(chain, authType);
070        }
071
072        @Override
073        public X509Certificate[] getAcceptedIssuers() {
074            return trustManager.getAcceptedIssuers();
075        }
076
077        /**
078         * Checks whether a host name matches the provided pattern. It accepts
079         * the use of wildcards in the pattern, e.g. {@code *.example.com}.
080         *
081         * @param hostName
082         *            The host name.
083         * @param pattern
084         *            The host name pattern, which may contain wild cards.
085         * @return {@code true} if the host name matched the pattern, otherwise
086         *         {@code false}.
087         */
088        private boolean hostNameMatchesPattern(final String hostName, final String pattern) {
089            final String[] nameElements = hostName.split("\\.");
090            final String[] patternElements = pattern.split("\\.");
091
092            boolean hostMatch = nameElements.length == patternElements.length;
093            for (int i = 0; i < nameElements.length && hostMatch; i++) {
094                final String ne = nameElements[i];
095                final String pe = patternElements[i];
096                if (!pe.equals("*")) {
097                    hostMatch = ne.equalsIgnoreCase(pe);
098                }
099            }
100            return hostMatch;
101        }
102
103        private void verifyHostName(final X509Certificate[] chain) {
104            try {
105                // TODO: NPE if root DN.
106                final DN dn =
107                        DN.valueOf(chain[0].getSubjectX500Principal().getName(), Schema
108                                .getCoreSchema());
109                final String certSubjectHostName =
110                        dn.iterator().next().iterator().next().getAttributeValue().toString();
111                if (!hostNameMatchesPattern(hostName, certSubjectHostName)) {
112                    throw new CertificateException(
113                            "The host name contained in the certificate chain subject DN \'"
114                                    + chain[0].getSubjectX500Principal()
115                                    + "' does not match the host name \'" + hostName + "'");
116                }
117            } catch (final Throwable t) {
118                LOG.log(Level.WARNING, "Error parsing subject dn: "
119                        + chain[0].getSubjectX500Principal(), t);
120            }
121        }
122    }
123
124    /** An X509TrustManager which rejects certificates which have expired or are not yet valid. */
125    private static final class CheckValidityDates implements X509TrustManager {
126
127        private final X509TrustManager trustManager;
128
129        private CheckValidityDates(final X509TrustManager trustManager) {
130            this.trustManager = trustManager;
131        }
132
133        @Override
134        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
135                throws CertificateException {
136            verifyExpiration(chain);
137            trustManager.checkClientTrusted(chain, authType);
138        }
139
140        @Override
141        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
142                throws CertificateException {
143            verifyExpiration(chain);
144            trustManager.checkServerTrusted(chain, authType);
145        }
146
147        @Override
148        public X509Certificate[] getAcceptedIssuers() {
149            return trustManager.getAcceptedIssuers();
150        }
151
152        private void verifyExpiration(final X509Certificate[] chain) throws CertificateException {
153            final Date currentDate = new Date();
154            for (final X509Certificate c : chain) {
155                try {
156                    c.checkValidity(currentDate);
157                } catch (final CertificateExpiredException e) {
158                    LOG.log(Level.WARNING, "Refusing to trust security" + " certificate \""
159                            + c.getSubjectDN().getName() + "\" because it" + " expired on "
160                            + String.valueOf(c.getNotAfter()));
161
162                    throw e;
163                } catch (final CertificateNotYetValidException e) {
164                    LOG.log(Level.WARNING, "Refusing to trust security" + " certificate \""
165                            + c.getSubjectDN().getName() + "\" because it" + " is not valid until "
166                            + String.valueOf(c.getNotBefore()));
167
168                    throw e;
169                }
170            }
171        }
172    }
173
174    /** An X509TrustManager which does not trust any certificates. */
175    private static final class DistrustAll implements X509TrustManager {
176        /** Single instance. */
177        private static final DistrustAll INSTANCE = new DistrustAll();
178
179        /** Prevent instantiation. */
180        private DistrustAll() {
181            // Nothing to do.
182        }
183
184        @Override
185        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
186                throws CertificateException {
187            throw new CertificateException();
188        }
189
190        @Override
191        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
192                throws CertificateException {
193            throw new CertificateException();
194        }
195
196        @Override
197        public X509Certificate[] getAcceptedIssuers() {
198            return new X509Certificate[0];
199        }
200    }
201
202    /** An X509TrustManager which trusts all certificates. */
203    private static final class TrustAll implements X509TrustManager {
204
205        /** Single instance. */
206        private static final TrustAll INSTANCE = new TrustAll();
207
208        /** Prevent instantiation. */
209        private TrustAll() {
210            // Nothing to do.
211        }
212
213        @Override
214        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
215                throws CertificateException {
216        }
217
218        @Override
219        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
220                throws CertificateException {
221        }
222
223        @Override
224        public X509Certificate[] getAcceptedIssuers() {
225            return new X509Certificate[0];
226        }
227    }
228
229    private static final Logger LOG = Logger.getLogger(TrustManagers.class.getName());
230
231    /**
232     * Wraps the provided {@code X509TrustManager} by adding additional
233     * validation which rejects certificate chains whose subject DN does not
234     * match the specified host name pattern. The pattern may contain
235     * wild-cards, for example {@code *.example.com}.
236     *
237     * @param hostName
238     *            A host name which the RDN value contained in
239     *            certificate subject DNs must match.
240     * @param trustManager
241     *            The trust manager to be wrapped.
242     * @return The wrapped trust manager.
243     * @throws NullPointerException
244     *             If {@code trustManager} or {@code hostNamePattern} was
245     *             {@code null}.
246     */
247    public static X509TrustManager checkHostName(final String hostName,
248            final X509TrustManager trustManager) {
249        Reject.ifNull(trustManager, hostName);
250        return new CheckHostName(trustManager, hostName);
251    }
252
253    /**
254     * Creates a new {@code X509TrustManager} which will use the named trust
255     * store file to determine whether to trust a certificate. It will use the
256     * default trust store format for the JVM (e.g. {@code JKS}) and will not
257     * use a password to open the trust store.
258     *
259     * @param file
260     *            The trust store file name.
261     * @return A new {@code X509TrustManager} which will use the named trust
262     *         store file to determine whether to trust a certificate.
263     * @throws GeneralSecurityException
264     *             If the trust store could not be loaded, perhaps due to
265     *             incorrect format, or missing algorithms.
266     * @throws IOException
267     *             If the trust store file could not be found or could not be
268     *             read.
269     * @throws NullPointerException
270     *             If {@code file} was {@code null}.
271     */
272    public static X509TrustManager checkUsingTrustStore(final String file)
273            throws GeneralSecurityException, IOException {
274        return checkUsingTrustStore(file, null, null);
275    }
276
277    /**
278     * Creates a new {@code X509TrustManager} which will use the named trust
279     * store file to determine whether to trust a certificate. It will use the
280     * provided trust store format and password.
281     *
282     * @param file
283     *            The trust store file name.
284     * @param password
285     *            The trust store password, which may be {@code null}.
286     * @param format
287     *            The trust store format, which may be {@code null} to indicate
288     *            that the default trust store format for the JVM (e.g.
289     *            {@code JKS}) should be used.
290     * @return A new {@code X509TrustManager} which will use the named trust
291     *         store file to determine whether to trust a certificate.
292     * @throws GeneralSecurityException
293     *             If the trust store could not be loaded, perhaps due to
294     *             incorrect format, or missing algorithms.
295     * @throws IOException
296     *             If the trust store file could not be found or could not be
297     *             read.
298     * @throws NullPointerException
299     *             If {@code file} was {@code null}.
300     */
301    public static X509TrustManager checkUsingTrustStore(final String file, final char[] password,
302            final String format) throws GeneralSecurityException, IOException {
303        Reject.ifNull(file);
304
305        final File trustStoreFile = new File(file);
306        final String trustStoreFormat = format != null ? format : KeyStore.getDefaultType();
307
308        final KeyStore keyStore = KeyStore.getInstance(trustStoreFormat);
309        try (FileInputStream fos = new FileInputStream(trustStoreFile)) {
310            keyStore.load(fos, password);
311        }
312
313        final TrustManagerFactory tmf =
314                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
315        tmf.init(keyStore);
316
317        X509TrustManager x509tm = null;
318        for (final TrustManager tm : tmf.getTrustManagers()) {
319            if (tm instanceof X509TrustManager) {
320                x509tm = (X509TrustManager) tm;
321                break;
322            }
323        }
324
325        if (x509tm == null) {
326            throw new NoSuchAlgorithmException();
327        }
328
329        return x509tm;
330    }
331
332    /**
333     * Wraps the provided {@code X509TrustManager} by adding additional
334     * validation which rejects certificate chains containing certificates which
335     * have expired or are not yet valid.
336     *
337     * @param trustManager
338     *            The trust manager to be wrapped.
339     * @return The wrapped trust manager.
340     * @throws NullPointerException
341     *             If {@code trustManager} was {@code null}.
342     */
343    public static X509TrustManager checkValidityDates(final X509TrustManager trustManager) {
344        Reject.ifNull(trustManager);
345        return new CheckValidityDates(trustManager);
346    }
347
348    /**
349     * Returns an {@code X509TrustManager} which does not trust any
350     * certificates.
351     *
352     * @return An {@code X509TrustManager} which does not trust any
353     *         certificates.
354     */
355    public static X509TrustManager distrustAll() {
356        return DistrustAll.INSTANCE;
357    }
358
359    /**
360     * Returns an {@code X509TrustManager} which trusts all certificates.
361     *
362     * @return An {@code X509TrustManager} which trusts all certificates.
363     */
364    public static X509TrustManager trustAll() {
365        return TrustAll.INSTANCE;
366    }
367
368    /** Prevent insantiation. */
369    private TrustManagers() {
370        // Nothing to do.
371    }
372
373}