/*
* Copyright (c) 2012 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package eu.emi.security.authn.x509.helpers.ssl;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.util.IPAddress;
import eu.emi.security.authn.x509.helpers.CertificateHelpers;
import eu.emi.security.authn.x509.impl.CertificateUtils;
/**
* Verifies if a peer's host name matches a DN of its certificate. It is useful on client side
* when connecting to a server.
* <p>
* By default the implementation checks the certificate's Subject Alternative Name
* and Common Name, following the server identity part of RFC 2818. Additionally the
* 'service/hostname' syntax is supported (the service prefix is simply ignored).
* <p>
* If there is a name mismatch the nameMismatch() method is called.
* User of this class must extend it and provide the application specific reaction
* in this method.
* <p>
* Note that this class should be used only on SSL connections which are
* authenticated with X.509 certificates.
*
* @author Joni Hahkala
* @author K. Benedyczak
*/
public class HostnameToCertificateChecker
{
static
{
CertificateUtils.configureSecProvider();
}
protected static class ResultWrapper
{
private boolean result = false;
}
public boolean checkMatching(String hostname, X509Certificate certificate)
throws CertificateParsingException, UnknownHostException
{
ResultWrapper result = new ResultWrapper();
if (checkAltNameMatching(result, hostname, certificate))
return result.result;
return checkCNMatching(hostname, certificate);
}
/**
*
* @param result result
* @param hostname hostname
* @param certificate certificate
* @return true iff a dNSName in altName was found (not if the matching was successful)
* RFC is unclear whether IP AltName presence is also taking the precedence over CN
* so we are not enforcing such a rule.
* @throws CertificateParsingException certificate parsing exception
* @throws UnknownHostException unknown host exception
*/
protected boolean checkAltNameMatching(ResultWrapper result, String hostname,
X509Certificate certificate) throws CertificateParsingException, UnknownHostException
{
Collection<List<?>> collection = certificate.getSubjectAlternativeNames();
if (collection == null)
return false;
boolean ipAsHostname = IPAddress.isValid(hostname);
boolean applicable = false;
Iterator<List<?>> collIter = collection.iterator();
while (collIter.hasNext())
{
List<?> item = collIter.next();
int type = ((Integer) item.get(0)).intValue();
if (type == GeneralName.dNSName)
{
applicable = true;
if (!ipAsHostname)
{
String dnsName = (String) item.get(1);
if (matchesDNS(hostname, dnsName))
{
result.result = true;
return applicable;
}
}
} else if (type == GeneralName.iPAddress && ipAsHostname)
{
String ipString = (String) item.get(1);
if (matchesIP(hostname, ipString))
{
result.result = true;
return applicable;
}
}
}
return applicable;
}
/**
*
* @param hostname hostname
* @param certificate certificate
* @return true if a CN was found and the matching was successful ;-)
*/
protected boolean checkCNMatching(String hostname, X509Certificate certificate)
{
X500Principal principal = certificate.getSubjectX500Principal();
if ("".equals(principal.getName()))
return false;
String cnValue = getMostSpecificCN(principal);
if (cnValue == null)
return false;
int index = cnValue.indexOf('/');
if (index >= 0)
cnValue = cnValue.substring(index + 1, cnValue.length());
return matchesDNS(hostname, cnValue);
}
public static boolean matchesDNS(String hostname, String pattern)
{
String regexp = makeRegexpHostWildcard(pattern);
Pattern p = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE);
return p.matcher(hostname).matches();
}
/**
* Converts hostname wildcard string to Java regexp, ensuring that
* literal sequences are correctly escaped.
* @param pattern hostname wildcard
* @return Java regular expression
*/
public static String makeRegexpHostWildcard(String pattern)
{
String[] rPNames = pattern.split("\\*");
StringBuilder patternB = new StringBuilder();
if (pattern.startsWith("*"))
patternB.append("[^\\.]*");
for (int i=0; i<rPNames.length; i++)
{
patternB.append(Pattern.quote(rPNames[i]));
if (i+1<rPNames.length)
patternB.append("[^\\.]*");
}
if (pattern.endsWith("*"))
patternB.append("[^\\.]*");
return patternB.toString();
}
protected boolean matchesIP(String what, String pattern) throws UnknownHostException
{
byte[] addr1 = InetAddress.getByName(what).getAddress();
byte[] addr2 = InetAddress.getByName(pattern).getAddress();
return Arrays.equals(addr1, addr2);
}
public String getMostSpecificCN(X500Principal srcP)
{
X500Name src = CertificateHelpers.toX500Name(srcP);
RDN[] srcRDNs = src.getRDNs();
String ret = null;
for (RDN rdn: srcRDNs)
{
if (rdn.isMultiValued())
continue;
AttributeTypeAndValue ava = rdn.getFirst();
if (ava.getType().equals(BCStyle.CN))
ret = IETFUtils.valueToString(ava.getValue());
}
return ret;
}
}