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.net.InetAddress;
023import java.net.UnknownHostException;
024import java.security.GeneralSecurityException;
025import java.security.KeyStore;
026import java.security.NoSuchAlgorithmException;
027import java.security.cert.CertificateException;
028import java.security.cert.CertificateExpiredException;
029import java.security.cert.CertificateNotYetValidException;
030import java.security.cert.CertificateParsingException;
031import java.security.cert.X509Certificate;
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Date;
035import java.util.List;
036import java.util.Set;
037
038import javax.net.ssl.TrustManager;
039import javax.net.ssl.TrustManagerFactory;
040import javax.net.ssl.X509TrustManager;
041import javax.security.auth.x500.X500Principal;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.forgerock.i18n.slf4j.LocalizedLogger;
045import org.forgerock.opendj.ldap.schema.AttributeType;
046import org.forgerock.opendj.ldap.schema.Schema;
047import org.forgerock.util.Reject;
048
049import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_IP;
050import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_DNS;
051import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_ALLOTHERS;
052import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_SUBJECT;
053
054
055/** This class contains methods for creating common types of trust manager. */
056public final class TrustManagers {
057
058    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
059
060    /**
061     * An X509TrustManager which rejects certificate chains whose subject alternative names do not match the specified
062     * host name or IP address. The check may fall back to checking a hostname in the left-most CN of the certificate
063     * subject for backwards compatibility.
064     */
065    private static final class CheckHostName implements X509TrustManager {
066
067        private final X509TrustManager trustManager;
068
069        private final String hostName;
070
071        private CheckHostName(final X509TrustManager trustManager, final String hostName) {
072            this.trustManager = trustManager;
073            this.hostName = hostName;
074        }
075
076        @Override
077        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
078                throws CertificateException {
079            verifyHostName(chain);
080            trustManager.checkClientTrusted(chain, authType);
081        }
082
083        @Override
084        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
085                throws CertificateException {
086            verifyHostName(chain);
087            trustManager.checkServerTrusted(chain, authType);
088        }
089
090        @Override
091        public X509Certificate[] getAcceptedIssuers() {
092            return trustManager.getAcceptedIssuers();
093        }
094
095        /**
096         * Look in the SubjectAlternativeName for DNS names (wildcards are allowed) and IP addresses, and potentially
097         * fall back to checking CN in the subjectDN.
098         * <p>
099         * If DNS names and IP addresses do not match, and other SubjectAlternativeNames are present and critical, do
100         * not fall back checking CN.
101         * </p>
102         * <p>
103         * If DNS names and IP addresses do not match and the SubjectAlternativeNames are non-critical, fall back to
104         * checking CN.
105         * </p>
106         * @param chain X.509 certificate chain from the server
107         */
108        private void verifyHostName(final X509Certificate[] chain) throws CertificateException {
109            final X500Principal principal = chain[0].getSubjectX500Principal();
110            try {
111                final List<String> dnsNamePatterns = new ArrayList<>(0);
112                final List<String> ipAddresses = new ArrayList<>(0);
113                final List<Object> allOthers = new ArrayList<>(0);
114                getSanGeneralNames(chain[0], dnsNamePatterns, ipAddresses, allOthers);
115                final boolean sanIsCritical = getSanCriticality(chain[0]);
116
117                final InetAddress hostAddress = toIpAddress(hostName);
118                if (hostAddress != null) {
119                    if (verifyIpAddresses(hostAddress, ipAddresses, principal, sanIsCritical)) {
120                        return;
121                    }
122                } else {
123                    if (verifyDnsNamePatterns(hostName, dnsNamePatterns, principal, sanIsCritical)) {
124                        return;
125                    }
126                }
127                if (!allOthers.isEmpty() && sanIsCritical) {
128                    throw new CertificateException(ERR_CERT_NO_MATCH_ALLOTHERS.get(principal, hostName).toString());
129                }
130
131                final DN dn = DN.valueOf(principal.getName(), Schema.getCoreSchema());
132                final String certSubjectHostName = getLowestCommonName(dn);
133                /* Backwards compatibility: check wildcards in cn */
134                if (hostNameMatchesPattern(hostName, certSubjectHostName)) {
135                    return;
136                }
137                throw new CertificateException(ERR_CERT_NO_MATCH_SUBJECT.get(principal, hostName).toString());
138            } catch (final CertificateException e) {
139                logger.warn(LocalizableMessage.raw("Certificate verification problem for: %s", principal), e);
140                throw e;
141            }
142        }
143
144        /**
145         * Collect the general names from a certificate's SubjectAlternativeName extension.
146         *
147         * General Names can contain: dnsNames, ipAddresses, rfc822Names, x400Addresses, directoryNames, ediPartyNames,
148         * uniformResourceIdentifiers, registeredIDs (OID), or otherNames (anything). See
149         * {@link X509Certificate#getSubjectAlternativeNames()} for details on how these values are encoded. We separate
150         * the dnsNames and ipAddresses (which we can try to match) from everything else (which we do not try to match.)
151         *
152         * @param subject  certificate
153         * @param dnsNames  list where the dnsNames will be added (may be empty)
154         * @param ipAddresses  list where the ipAddresses will be added (may be empty)
155         * @param allOthers  list where all other general names will be added (may be empty)
156         */
157        private void getSanGeneralNames(X509Certificate subject,
158                                        List<String> dnsNames, List<String> ipAddresses,
159                                        List<Object> allOthers) {
160            try {
161                Collection<List<?>> sans = subject.getSubjectAlternativeNames();
162                if (sans == null) {
163                    return;
164                }
165                for (List<?> san : sans) {
166                    switch ((Integer) san.get(0)) {
167                    case 2:
168                        dnsNames.add((String) san.get(1));
169                        break;
170                    case 7:
171                        ipAddresses.add((String) san.get(1));
172                        break;
173                    default:
174                        allOthers.add(san.get(1));
175                        break;
176                    }
177                }
178            } catch (CertificateParsingException e) {
179                /* do nothing */
180            }
181        }
182
183        /**
184         * Get the ASN.1 criticality of the SubjectAlternativeName extension.
185         *
186         * @param subject X509Certificate to check
187         * @return {@code true} if a subject alt name was found and was marked critical, {@code false} otherwise.
188         */
189        private boolean getSanCriticality(X509Certificate subject) {
190            Set<String> critSet = subject.getCriticalExtensionOIDs();
191            return critSet != null && critSet.contains("2.5.29.17");
192        }
193
194        /**
195         * Convert to an IP address without performing a DNS lookup.
196         *
197         * @param hostName  either an IP address string, or a host name
198         * @return {@code InetAddress} if hostName was an IPv4 or IPv6 address, or {@code null}
199         */
200        private static InetAddress toIpAddress(String hostName) {
201            try {
202                if (InetAddressValidator.isValid(hostName)) {
203                    return InetAddress.getByName(hostName);
204                }
205            } catch (UnknownHostException e) {
206                /* do nothing */
207            }
208            return null;
209        }
210
211        /**
212         * Verify an IP address in the list of IP addresses.
213         *
214         * @param hostAddress  IP address from the user
215         * @param ipAddresses  List of IP addresses from the certificate (may be empty)
216         * @param principal  Subject name from the certificate
217         * @param failureIsCritical  Should a verification failure throw a {@link CertificateException}
218         * @return {@code true} if the address is verified, {@code false} if the address was not verified.
219         * @throws CertificateException  if verification fails and {@code failureIsCritical} is {@code true}.
220         */
221        private boolean verifyIpAddresses(InetAddress hostAddress, List<String> ipAddresses, X500Principal principal,
222                                          boolean failureIsCritical) throws CertificateException {
223            if (!ipAddresses.isEmpty()) {
224                for (String address : ipAddresses) {
225                    try {
226                        if (InetAddress.getByName(address).equals(hostAddress)) {
227                            return true;
228                        }
229                    } catch (UnknownHostException e) {
230                        // do nothing
231                    }
232                }
233                if (failureIsCritical) {
234                    // RFC 5280 mentions:
235                            /* If the subject field
236                             * contains an empty sequence, then the issuing CA MUST include a
237                             * subjectAltName extension that is marked as critical.  When including
238                             * the subjectAltName extension in a certificate that has a non-empty
239                             * subject distinguished name, conforming CAs SHOULD mark the
240                             * subjectAltName extension as non-critical.
241                             */
242                    // Since SAN is critical, the subject is empty, so we cannot perform the next check anyway
243                    throw new CertificateException(ERR_CERT_NO_MATCH_IP.get(principal, hostName).toString());
244                }
245            }
246            return false;
247        }
248
249        /**
250         * Verify a hostname in the list of DNS name patterns.
251         *
252         * @param hostName  Host name from the user
253         * @param dnsNamePatterns  List of DNS name patterns from the certificate (may be empty)
254         * @param principal  Subject name from the certificate
255         * @param failureIsCritical  Should a verification failure throw a {@link CertificateException}
256         * @return {@code true} if the address is verified, {@code false} if the address was not verified.
257         * @throws CertificateException  If verification fails and {@code failureIsCritical} is {@code true}
258         */
259        private boolean verifyDnsNamePatterns(String hostName, List<String> dnsNamePatterns, X500Principal principal,
260                              boolean failureIsCritical) throws CertificateException {
261            for (String namePattern : dnsNamePatterns) {
262                if (hostNameMatchesPattern(hostName, namePattern)) {
263                    return true;
264                }
265            }
266            if (failureIsCritical) {
267                throw new CertificateException(ERR_CERT_NO_MATCH_DNS.get(principal, hostName).toString());
268            }
269            return false;
270        }
271
272        /**
273         * Checks whether a host name matches the provided pattern. It accepts the use of wildcards in the pattern,
274         * e.g. {@code *.example.com}.
275         *
276         * @param hostName
277         *            The host name.
278         * @param pattern
279         *            The host name pattern, which may contain wildcards.
280         * @return {@code true} if the host name matched the pattern, otherwise {@code false}.
281         */
282        private boolean hostNameMatchesPattern(final String hostName, final String pattern) {
283            final String[] nameElements = hostName.split("\\.");
284            final String[] patternElements = pattern.split("\\.");
285
286            boolean hostMatch = nameElements.length == patternElements.length;
287            for (int i = 0; i < nameElements.length && hostMatch; i++) {
288                final String ne = nameElements[i];
289                final String pe = patternElements[i];
290                if (!pe.equals("*")) {
291                    hostMatch = ne.equalsIgnoreCase(pe);
292                }
293            }
294            return hostMatch;
295        }
296
297        /**
298         * Find the lowest (left-most) cn in the DN, and return its value.
299         *
300         * @param subject the DN being searched
301         * @return the cn value, or {@code null} if no cn was found
302         */
303        private String getLowestCommonName(DN subject) {
304            AttributeType cn = Schema.getDefaultSchema().getAttributeType("cn");
305            for (RDN rdn : subject) {
306                for (AVA ava : rdn) {
307                    if (ava.getAttributeType().equals(cn)) {
308                        return ava.getAttributeValue().toString();
309                    }
310                }
311            }
312            return null;
313        }
314    }
315
316    /** An X509TrustManager which rejects certificates which have expired or are not yet valid. */
317    private static final class CheckValidityDates implements X509TrustManager {
318
319        private final X509TrustManager trustManager;
320
321        private CheckValidityDates(final X509TrustManager trustManager) {
322            this.trustManager = trustManager;
323        }
324
325        @Override
326        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
327                throws CertificateException {
328            verifyExpiration(chain);
329            trustManager.checkClientTrusted(chain, authType);
330        }
331
332        @Override
333        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
334                throws CertificateException {
335            verifyExpiration(chain);
336            trustManager.checkServerTrusted(chain, authType);
337        }
338
339        @Override
340        public X509Certificate[] getAcceptedIssuers() {
341            return trustManager.getAcceptedIssuers();
342        }
343
344        private void verifyExpiration(final X509Certificate[] chain) throws CertificateException {
345            final Date currentDate = new Date();
346            for (final X509Certificate c : chain) {
347                try {
348                    c.checkValidity(currentDate);
349                } catch (final CertificateExpiredException e) {
350                    logger.warn(LocalizableMessage.raw(
351                            "Refusing to trust security certificate \'%s\' because it expired on %s",
352                            c.getSubjectDN().getName(), c.getNotAfter()));
353                    throw e;
354                } catch (final CertificateNotYetValidException e) {
355                    logger.warn(LocalizableMessage.raw(
356                            "Refusing to trust security  certificate \'%s\' because it is not valid until %s",
357                            c.getSubjectDN().getName(), c.getNotBefore()));
358                    throw e;
359                }
360            }
361        }
362    }
363
364    /** An X509TrustManager which does not trust any certificates. */
365    private static final class DistrustAll implements X509TrustManager {
366        /** Single instance. */
367        private static final DistrustAll INSTANCE = new DistrustAll();
368
369        /** Prevent instantiation. */
370        private DistrustAll() {
371            // Nothing to do.
372        }
373
374        @Override
375        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
376                throws CertificateException {
377            throw new CertificateException();
378        }
379
380        @Override
381        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
382                throws CertificateException {
383            throw new CertificateException();
384        }
385
386        @Override
387        public X509Certificate[] getAcceptedIssuers() {
388            return new X509Certificate[0];
389        }
390    }
391
392    /** An X509TrustManager which trusts all certificates. */
393    private static final class TrustAll implements X509TrustManager {
394        /** Single instance. */
395        private static final TrustAll INSTANCE = new TrustAll();
396
397        /** Prevent instantiation. */
398        private TrustAll() {
399            // Nothing to do.
400        }
401
402        @Override
403        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
404                throws CertificateException {
405        }
406
407        @Override
408        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
409                throws CertificateException {
410        }
411
412        @Override
413        public X509Certificate[] getAcceptedIssuers() {
414            return new X509Certificate[0];
415        }
416    }
417
418    /**
419     * Wraps the provided {@code X509TrustManager} by adding additional validation which rejects certificate chains
420     * whose subject alternative names do not match the specified host name or IP address. The check may fall back to
421     * checking a hostname in the left-most CN of the subjectDN for backwards compatibility.
422     *
423     * If the {@code hostName} is an IP address, only the {@code ipAddresses} field of the subject alternative name
424     * will be checked. Similarly if {@code hostName} is not an IP address, only the {@code dnsNames} of the subject
425     * alternative name will be checked.
426     *
427     * Host names can be matched using wild cards, for example {@code *.example.com}.
428     *
429     * If a critical subject alternative name doesn't match, verification will not fall back to checking the subjectDN
430     * and will <b>fail</b>. If a critical subject alternative name doesn't match and it contains other kinds of general
431     * names that cannot be checked verification will also <b>fail</b>.
432     *
433     * @param hostName
434     *            The IP address or hostname used to connect to the LDAP server which will be matched against the
435     *            subject alternative name and possibly the subjectDN as described above.
436     * @param trustManager
437     *            The trust manager to be wrapped.
438     * @return The wrapped trust manager.
439     * @throws NullPointerException
440     *             If {@code trustManager} or {@code hostName} was {@code null}.
441     */
442    public static X509TrustManager checkHostName(final String hostName,
443            final X509TrustManager trustManager) {
444        Reject.ifNull(trustManager, hostName);
445        return new CheckHostName(trustManager, hostName);
446    }
447
448    /**
449     * Creates a new {@code X509TrustManager} which will use the named trust
450     * store file to determine whether to trust a certificate. It will use the
451     * default trust store format for the JVM (e.g. {@code JKS}) and will not
452     * use a password to open the trust store.
453     *
454     * @param file
455     *            The trust store file name.
456     * @return A new {@code X509TrustManager} which will use the named trust
457     *         store file to determine whether to trust a certificate.
458     * @throws GeneralSecurityException
459     *             If the trust store could not be loaded, perhaps due to
460     *             incorrect format, or missing algorithms.
461     * @throws IOException
462     *             If the trust store file could not be found or could not be
463     *             read.
464     * @throws NullPointerException
465     *             If {@code file} was {@code null}.
466     */
467    public static X509TrustManager checkUsingTrustStore(final String file)
468            throws GeneralSecurityException, IOException {
469        return checkUsingTrustStore(file, null, null);
470    }
471
472    /**
473     * Creates a new {@code X509TrustManager} which will use the named trust
474     * store file to determine whether to trust a certificate. It will use the
475     * provided trust store format and password.
476     *
477     * @param file
478     *            The trust store file name.
479     * @param password
480     *            The trust store password, which may be {@code null}.
481     * @param format
482     *            The trust store format, which may be {@code null} to indicate
483     *            that the default trust store format for the JVM (e.g.
484     *            {@code JKS}) should be used.
485     * @return A new {@code X509TrustManager} which will use the named trust
486     *         store file to determine whether to trust a certificate.
487     * @throws GeneralSecurityException
488     *             If the trust store could not be loaded, perhaps due to
489     *             incorrect format, or missing algorithms.
490     * @throws IOException
491     *             If the trust store file could not be found or could not be
492     *             read.
493     * @throws NullPointerException
494     *             If {@code file} was {@code null}.
495     */
496    public static X509TrustManager checkUsingTrustStore(final String file, final char[] password,
497            final String format) throws GeneralSecurityException, IOException {
498        Reject.ifNull(file);
499
500        final File trustStoreFile = new File(file);
501        final String trustStoreFormat = format != null ? format : KeyStore.getDefaultType();
502
503        final KeyStore keyStore = KeyStore.getInstance(trustStoreFormat);
504        try (FileInputStream fos = new FileInputStream(trustStoreFile)) {
505            keyStore.load(fos, password);
506        }
507
508        final TrustManagerFactory tmf =
509                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
510        tmf.init(keyStore);
511
512        for (final TrustManager tm : tmf.getTrustManagers()) {
513            if (tm instanceof X509TrustManager) {
514                return (X509TrustManager) tm;
515            }
516        }
517        throw new NoSuchAlgorithmException();
518    }
519
520    /**
521     * Wraps the provided {@code X509TrustManager} by adding additional
522     * validation which rejects certificate chains containing certificates which
523     * have expired or are not yet valid.
524     *
525     * @param trustManager
526     *            The trust manager to be wrapped.
527     * @return The wrapped trust manager.
528     * @throws NullPointerException
529     *             If {@code trustManager} was {@code null}.
530     */
531    public static X509TrustManager checkValidityDates(final X509TrustManager trustManager) {
532        Reject.ifNull(trustManager);
533        return new CheckValidityDates(trustManager);
534    }
535
536    /**
537     * Returns an {@code X509TrustManager} which does not trust any
538     * certificates.
539     *
540     * @return An {@code X509TrustManager} which does not trust any
541     *         certificates.
542     */
543    public static X509TrustManager distrustAll() {
544        return DistrustAll.INSTANCE;
545    }
546
547    /**
548     * Returns an {@code X509TrustManager} which trusts all certificates.
549     *
550     * @return An {@code X509TrustManager} which trusts all certificates.
551     */
552    public static X509TrustManager trustAll() {
553        return TrustAll.INSTANCE;
554    }
555
556    /** Prevent instantiation. */
557    private TrustManagers() {
558        // Nothing to do.
559    }
560}