/** * * Copyright 2015 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smack.java7; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.security.auth.kerberos.KerberosPrincipal; import org.minidns.util.InetAddressUtil; /** * HostnameVerifier implementation for XMPP. Verifies a given name, the 'hostname' argument, which * should be the XMPP domain of the used XMPP service. The verifier compares the name with the * servers TLS certificate's Subject * Alternative Name (SAN) DNS name ('dNSName'), and, if there are no SANs, which the Common Name * (CN). *
* Based on the work by Kevin * Locke (released under CC0 1.0 Universal / Public Domain Dedication). *
*/ public class XmppHostnameVerifier implements HostnameVerifier { private static final Logger LOGGER = Logger.getLogger(XmppHostnameVerifier.class.getName()); @Override public boolean verify(String hostname, SSLSession session) { boolean validCertificate = false, validPrincipal = false; try { Certificate[] peerCertificates = session.getPeerCertificates(); if (peerCertificates.length == 0) { return false; } if (!(peerCertificates[0] instanceof X509Certificate)) { return false; } X509Certificate peerCertificate = (X509Certificate) peerCertificates[0]; try { match(hostname, peerCertificate); // Certificate matches hostname validCertificate = true; } catch (CertificateException e) { LOGGER.log(Level.INFO, "Certificate does not match hostname", e); } } catch (SSLPeerUnverifiedException e) { // Not using certificates for peers, try verifying the principal Principal peerPrincipal = null; try { peerPrincipal = session.getPeerPrincipal(); } catch (SSLPeerUnverifiedException e2) { LOGGER.log(Level.INFO, "Can't verify principal for " + hostname + ". Not kerberos", e2); } if (peerPrincipal instanceof KerberosPrincipal) { validPrincipal = match(hostname, (KerberosPrincipal) peerPrincipal); } else { LOGGER.info("Can't verify principal for " + hostname + ". Not kerberos"); } } return validCertificate || validPrincipal; } private static void match(String name, X509Certificate cert) throws CertificateException { if (InetAddressUtil.isIpAddress(name)) { matchIp(name, cert); } else { matchDns(name, cert); } } private static boolean match(String name, KerberosPrincipal peerPrincipal) { // TODO LOGGER.warning("KerberosPrincipal '" + peerPrincipal + "' validation not implemented yet. Can not verify " + name); return false; } /** * As defined in RFC 5280 § 4.2.1.6 ** GeneralName ::= CHOICE { * ... * dNSName [2] IA5String, * ... * } **/ private static final int ALTNAME_DNS = 2; /** * Try to match a certificate with a DNS name. This method returns if the certificate matches or * throws a {@link CertificateException} if not. * * @param name the DNS name. * @param cert the certificate. * @throws CertificateException if the DNS name does not match the certificate. */ private static void matchDns(String name, X509Certificate cert) throws CertificateException { Collection
name
matches template
.
*/
private static boolean matchWildCards(String name, String template) {
int wildcardIndex = template.indexOf("*");
if (wildcardIndex == -1) {
return name.equals(template);
}
boolean isBeginning = true;
String beforeWildcard;
String afterWildcard = template;
while (wildcardIndex != -1) {
beforeWildcard = afterWildcard.substring(0, wildcardIndex);
afterWildcard = afterWildcard.substring(wildcardIndex + 1);
int beforeStartIndex = name.indexOf(beforeWildcard);
if ((beforeStartIndex == -1) || (isBeginning && beforeStartIndex != 0)) {
return false;
}
isBeginning = false;
name = name.substring(beforeStartIndex + beforeWildcard.length());
wildcardIndex = afterWildcard.indexOf("*");
}
return name.endsWith(afterWildcard);
}
private static final int ALTNAME_IP = 7;
/**
* Check if the certificate allows use of the given IP address.
* * From RFC2818 § 3.1: "In some cases, the URI is specified as an IP address rather than a * hostname. In this case, the iPAddress subjectAltName must be present in the certificate and * must exactly match the IP in the URI." *
*
* @param expectedIP TODO javadoc me please
* @param cert TODO javadoc me please
* @throws CertificateException
*/
private static void matchIp(String expectedIP, X509Certificate cert)
throws CertificateException {
Collection> subjectAlternativeNames = cert.getSubjectAlternativeNames();
if (subjectAlternativeNames == null) {
throw new CertificateException("No subject alternative names present");
}
List