001package lucee.runtime.net.http.sni;
002
003import java.net.InetAddress;
004import java.net.UnknownHostException;
005import java.security.cert.Certificate;
006import java.security.cert.CertificateParsingException;
007import java.security.cert.X509Certificate;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.List;
011import java.util.Locale;
012import java.util.NoSuchElementException;
013
014import javax.naming.InvalidNameException;
015import javax.naming.NamingException;
016import javax.naming.directory.Attribute;
017import javax.naming.directory.Attributes;
018import javax.naming.ldap.LdapName;
019import javax.naming.ldap.Rdn;
020import javax.net.ssl.HostnameVerifier;
021import javax.net.ssl.SSLException;
022import javax.net.ssl.SSLSession;
023import javax.security.auth.x500.X500Principal;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.apache.http.annotation.Immutable;
028import org.apache.http.conn.util.DomainType;
029import org.apache.http.conn.util.InetAddressUtils;
030import org.apache.http.conn.util.PublicSuffixMatcher;
031
032
033public class AbsDefaultHostnameVerifier implements HostnameVerifier {
034
035    final static int DNS_NAME_TYPE        = 2;
036    final static int IP_ADDRESS_TYPE      = 7;
037
038    private final Log log = LogFactory.getLog(getClass());
039
040    private final PublicSuffixMatcher publicSuffixMatcher;
041
042    public AbsDefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
043        this.publicSuffixMatcher = publicSuffixMatcher;
044    }
045
046    public AbsDefaultHostnameVerifier() {
047        this(null);
048    }
049
050    @Override
051    public boolean verify(final String host, final SSLSession session) {
052        try {
053            final Certificate[] certs = session.getPeerCertificates();
054            final X509Certificate x509 = (X509Certificate) certs[0];
055            verify(host, x509);
056            return true;
057        } catch (final SSLException ex) {
058            if (log.isDebugEnabled()) {
059                log.debug(ex.getMessage(), ex);
060            }
061            return false;
062        }
063    }
064
065    public void verify(
066            final String host, final X509Certificate cert) throws SSLException {
067        final boolean ipv4 = InetAddressUtils.isIPv4Address(host);
068        final boolean ipv6 = InetAddressUtils.isIPv6Address(host);
069        final int subjectType = ipv4 || ipv6 ? IP_ADDRESS_TYPE : DNS_NAME_TYPE;
070        final List<String> subjectAlts = extractSubjectAlts(cert, subjectType);
071        if (subjectAlts != null && !subjectAlts.isEmpty()) {
072            if (ipv4) {
073                matchIPAddress(host, subjectAlts);
074            } else if (ipv6) {
075                matchIPv6Address(host, subjectAlts);
076            } else {
077                matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
078            }
079        } else {
080            // CN matching has been deprecated by rfc2818 and can be used
081            // as fallback only when no subjectAlts are available
082            final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
083            final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
084            if (cn == null) {
085                throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
086                        "a common name and does not have alternative names");
087            }
088            matchCN(host, cn, this.publicSuffixMatcher);
089        }
090    }
091
092    static void matchIPAddress(final String host, final List<String> subjectAlts) throws SSLException {
093        for (int i = 0; i < subjectAlts.size(); i++) {
094            final String subjectAlt = subjectAlts.get(i);
095            if (host.equals(subjectAlt)) {
096                return;
097            }
098        }
099        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
100                "of the subject alternative names: " + subjectAlts);
101    }
102
103    static void matchIPv6Address(final String host, final List<String> subjectAlts) throws SSLException {
104        final String normalisedHost = normaliseAddress(host);
105        for (int i = 0; i < subjectAlts.size(); i++) {
106            final String subjectAlt = subjectAlts.get(i);
107            final String normalizedSubjectAlt = normaliseAddress(subjectAlt);
108            if (normalisedHost.equals(normalizedSubjectAlt)) {
109                return;
110            }
111        }
112        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
113                "of the subject alternative names: " + subjectAlts);
114    }
115
116    static void matchDNSName(final String host, final List<String> subjectAlts,
117                             final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
118        final String normalizedHost = host.toLowerCase(Locale.ROOT);
119        for (int i = 0; i < subjectAlts.size(); i++) {
120            final String subjectAlt = subjectAlts.get(i);
121            final String normalizedSubjectAlt = subjectAlt.toLowerCase(Locale.ROOT);
122            if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
123                return;
124            }
125        }
126        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
127                "of the subject alternative names: " + subjectAlts);
128    }
129
130    static void matchCN(final String host, final String cn,
131                 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
132        if (!matchIdentityStrict(host, cn, publicSuffixMatcher)) {
133            throw new SSLException("Certificate for <" + host + "> doesn't match " +
134                    "common name of the certificate subject: " + cn);
135        }
136    }
137
138    static boolean matchDomainRoot(final String host, final String domainRoot) {
139        if (domainRoot == null) {
140            return false;
141        }
142        return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
143                || host.charAt(host.length() - domainRoot.length() - 1) == '.');
144    }
145
146    private static boolean matchIdentity(final String host, final String identity,
147                                         final PublicSuffixMatcher publicSuffixMatcher,
148                                         final boolean strict) {
149        if (publicSuffixMatcher != null && host.contains(".")) {
150            if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) {
151                return false;
152            }
153        }
154
155        // RFC 2818, 3.1. Server Identity
156        // "...Names may contain the wildcard
157        // character * which is considered to match any single domain name
158        // component or component fragment..."
159        // Based on this statement presuming only singular wildcard is legal
160        final int asteriskIdx = identity.indexOf('*');
161        if (asteriskIdx != -1) {
162            final String prefix = identity.substring(0, asteriskIdx);
163            final String suffix = identity.substring(asteriskIdx + 1);
164            if (!prefix.isEmpty() && !host.startsWith(prefix)) {
165                return false;
166            }
167            if (!suffix.isEmpty() && !host.endsWith(suffix)) {
168                return false;
169            }
170            // Additional sanity checks on content selected by wildcard can be done here
171            if (strict) {
172                final String remainder = host.substring(
173                        prefix.length(), host.length() - suffix.length());
174                if (remainder.contains(".")) {
175                    return false;
176                }
177            }
178            return true;
179        }
180        return host.equalsIgnoreCase(identity);
181    }
182
183    static boolean matchIdentity(final String host, final String identity,
184                                 final PublicSuffixMatcher publicSuffixMatcher) {
185        return matchIdentity(host, identity, publicSuffixMatcher, false);
186    }
187
188    static boolean matchIdentity(final String host, final String identity) {
189        return matchIdentity(host, identity, null, false);
190    }
191
192    static boolean matchIdentityStrict(final String host, final String identity,
193                                       final PublicSuffixMatcher publicSuffixMatcher) {
194        return matchIdentity(host, identity, publicSuffixMatcher, true);
195    }
196
197    static boolean matchIdentityStrict(final String host, final String identity) {
198        return matchIdentity(host, identity, null, true);
199    }
200
201    static String extractCN(final String subjectPrincipal) throws SSLException {
202        if (subjectPrincipal == null) {
203            return null;
204        }
205        try {
206            final LdapName subjectDN = new LdapName(subjectPrincipal);
207            final List<Rdn> rdns = subjectDN.getRdns();
208            for (int i = rdns.size() - 1; i >= 0; i--) {
209                final Rdn rds = rdns.get(i);
210                final Attributes attributes = rds.toAttributes();
211                final Attribute cn = attributes.get("cn");
212                if (cn != null) {
213                    try {
214                        final Object value = cn.get();
215                        if (value != null) {
216                            return value.toString();
217                        }
218                    } catch (NoSuchElementException ignore) {
219                    } catch (NamingException ignore) {
220                    }
221                }
222            }
223            return null;
224        } catch (InvalidNameException e) {
225            throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
226        }
227    }
228
229    static List<String> extractSubjectAlts(final X509Certificate cert, final int subjectType) {
230        Collection<List<?>> c = null;
231        try {
232            c = cert.getSubjectAlternativeNames();
233        } catch(final CertificateParsingException ignore) {
234        }
235        List<String> subjectAltList = null;
236        if (c != null) {
237            for (final List<?> aC : c) {
238                final List<?> list = aC;
239                final int type = ((Integer) list.get(0)).intValue();
240                if (type == subjectType) {
241                    final String s = (String) list.get(1);
242                    if (subjectAltList == null) {
243                        subjectAltList = new ArrayList<String>();
244                    }
245                    subjectAltList.add(s);
246                }
247            }
248        }
249        return subjectAltList;
250    }
251
252    /*
253     * Normalize IPv6 or DNS name.
254     */
255    static String normaliseAddress(final String hostname) {
256        if (hostname == null) {
257            return hostname;
258        }
259        try {
260            final InetAddress inetAddress = InetAddress.getByName(hostname);
261            return inetAddress.getHostAddress();
262        } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
263            return hostname;
264        }
265    }
266}