/* * 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; } }