Added support for SubjectAltName and "SSL hostname" matching from credentials to...
authorwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 27 Apr 2004 04:29:22 +0000 (04:29 +0000)
committerwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 27 Apr 2004 04:29:22 +0000 (04:29 +0000)
Refactored SSL hostname hack and added some unit tests for the same.

git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@1018 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/aa/AAServlet.java
src/edu/internet2/middleware/shibboleth/common/ShibPOSTProfile.java
tests/edu/internet2/middleware/shibboleth/aa/DNHostNameExtractionTests.java [new file with mode: 0644]

index 45df278..1695774 100755 (executable)
@@ -30,15 +30,19 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.Principal;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.Principal;
+import java.security.cert.CertificateParsingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.security.auth.x500.X500Principal;
 import javax.servlet.ServletException;
 import javax.servlet.UnavailableException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.ServletException;
 import javax.servlet.UnavailableException;
 import javax.servlet.http.HttpServletRequest;
@@ -83,6 +87,7 @@ import edu.internet2.middleware.shibboleth.common.OriginConfig;
 import edu.internet2.middleware.shibboleth.common.RelyingParty;
 import edu.internet2.middleware.shibboleth.common.SAMLBindingFactory;
 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
 import edu.internet2.middleware.shibboleth.common.RelyingParty;
 import edu.internet2.middleware.shibboleth.common.SAMLBindingFactory;
 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
+import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
 import edu.internet2.middleware.shibboleth.common.ShibbolethOriginConfig;
 import edu.internet2.middleware.shibboleth.common.TargetFederationComponent;
 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
 import edu.internet2.middleware.shibboleth.common.ShibbolethOriginConfig;
 import edu.internet2.middleware.shibboleth.common.TargetFederationComponent;
@@ -105,7 +110,6 @@ public class AAServlet extends TargetFederationComponent {
        private AAServiceProviderMapper targetMapper;
 
        private static Logger                   log                             = Logger.getLogger(AAServlet.class.getName());
        private AAServiceProviderMapper targetMapper;
 
        private static Logger                   log                             = Logger.getLogger(AAServlet.class.getName());
-       protected Pattern                               regex                   = Pattern.compile(".*CN=([^,/]+).*");
 
        public void init() throws ServletException {
                super.init();
 
        public void init() throws ServletException {
                super.init();
@@ -246,8 +250,8 @@ public class AAServlet extends TargetFederationComponent {
                        //This is the requester name that will be passed to subsystems
                        String effectiveName = null;
 
                        //This is the requester name that will be passed to subsystems
                        String effectiveName = null;
 
-                       String credentialName = getCredentialName(req);
-                       if (credentialName == null || credentialName.toString().equals("")) {
+                       X509Certificate credential = getCredentialFromProvider(req);
+                       if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
                                log.info("Request is from an unauthenticated service provider.");
                        } else {
 
                                log.info("Request is from an unauthenticated service provider.");
                        } else {
 
@@ -393,16 +397,23 @@ public class AAServlet extends TargetFederationComponent {
        protected String getEffectiveName(HttpServletRequest req, AARelyingParty relyingParty)
                        throws InvalidProviderCredentialException {
 
        protected String getEffectiveName(HttpServletRequest req, AARelyingParty relyingParty)
                        throws InvalidProviderCredentialException {
 
-               String credentialName = getCredentialName(req);
-               if (credentialName == null || credentialName.toString().equals("")) {
+               //X500Principal credentialName = getCredentialName(req);
+               X509Certificate credential = getCredentialFromProvider(req);
+
+               if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
                        log.info("Request is from an unauthenticated service provider.");
                        return null;
 
                } else {
                        log.info("Request is from an unauthenticated service provider.");
                        return null;
 
                } else {
-                       log.info("Request contains credential: (" + credentialName + ").");
+                       log.info("Request contains credential: ("
+                                       + credential.getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
                        //Mockup old requester name for requests from < 1.2 targets
                        if (fromLegacyProvider(req)) {
                        //Mockup old requester name for requests from < 1.2 targets
                        if (fromLegacyProvider(req)) {
-                               String legacyName = getLegacyRequesterName(credentialName);
+                               String legacyName = ShibPOSTProfile.getHostNameFromDN(credential.getSubjectX500Principal());
+                               if (legacyName == null) {
+                                       log.error("Unable to extract legacy requester name from certificate subject.");
+                               }
+
                                log.info("Request from legacy service provider: (" + legacyName + ").");
                                return legacyName;
 
                                log.info("Request from legacy service provider: (" + legacyName + ").");
                                return legacyName;
 
@@ -417,28 +428,20 @@ public class AAServlet extends TargetFederationComponent {
                                }
 
                                //Make sure that the suppplied credential is valid for the selected relying party
                                }
 
                                //Make sure that the suppplied credential is valid for the selected relying party
-                               if (isValidCredential(provider, credentialName.toString())) {
+                               if (isValidCredential(provider, credential)) {
                                        log.info("Supplied credential validated for this provider.");
                                        log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
                                        return relyingParty.getProviderId();
                                } else {
                                        log.info("Supplied credential validated for this provider.");
                                        log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
                                        return relyingParty.getProviderId();
                                } else {
-                                       log.error("Supplied credential (" + credentialName.toString() + ") is NOT valid for provider ("
-                                                       + relyingParty.getProviderId() + ").");
+                                       log.error("Supplied credential ("
+                                                       + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
+                                                       + ") is NOT valid for provider (" + relyingParty.getProviderId() + ").");
                                        throw new InvalidProviderCredentialException("Invalid credential.");
                                }
                        }
                }
        }
 
                                        throw new InvalidProviderCredentialException("Invalid credential.");
                                }
                        }
                }
        }
 
-       private String getLegacyRequesterName(String credentialName) {
-               Matcher matches = regex.matcher(credentialName);
-               if (!matches.find() || matches.groupCount() > 1) {
-                       log.error("Unable to extract legacy requester name from certificate subject.");
-                       return null;
-               }
-               return matches.group(1);
-       }
-
        public void destroy() {
                log.info("Cleaning up resources.");
                responder.destroy();
        public void destroy() {
                log.info("Cleaning up resources.");
                responder.destroy();
@@ -585,7 +588,7 @@ public class AAServlet extends TargetFederationComponent {
                }
        }
 
                }
        }
 
-       protected boolean isValidCredential(Provider provider, String credentialName) {
+       protected boolean isValidCredential(Provider provider, X509Certificate certificate) {
 
                ProviderRole[] roles = provider.getRoles();
                if (roles.length == 0) {
 
                ProviderRole[] roles = provider.getRoles();
                if (roles.length == 0) {
@@ -601,9 +604,47 @@ public class AAServlet extends TargetFederationComponent {
                                        for (int k = 0; keyInfo.length > k; k++) {
                                                for (int l = 0; keyInfo[k].lengthKeyName() > l; l++) {
                                                        try {
                                        for (int k = 0; keyInfo.length > k; k++) {
                                                for (int l = 0; keyInfo[k].lengthKeyName() > l; l++) {
                                                        try {
-                                                               if (credentialName.equals(keyInfo[k].itemKeyName(l).getKeyName())) {
+
+                                                               //First, try to match DN against metadata
+                                                               try {
+                                                                       if (certificate.getSubjectX500Principal().getName(X500Principal.RFC2253).equals(
+                                                                                       new X500Principal(keyInfo[k].itemKeyName(l).getKeyName())
+                                                                                                       .getName(X500Principal.RFC2253))) {
+                                                                               log.debug("Matched against DN.");
+                                                                               return true;
+                                                                       }
+                                                               } catch (IllegalArgumentException iae) {
+                                                                       //squelch this runtime exception, since this might be a valid case
+                                                               }
+
+                                                               //If that doesn't work, we try matching against some Subject Alt Names
+                                                               try {
+                                                                       Collection altNames = certificate.getSubjectAlternativeNames();
+                                                                       if (altNames != null) {
+                                                                               for (Iterator nameIterator = altNames.iterator(); nameIterator.hasNext();) {
+                                                                                       List altName = (List) nameIterator.next();
+                                                                                       if (altName.get(0).equals(new Integer(2))
+                                                                                                       || altName.get(0).equals(new Integer(6))) { //2 is DNS, 6 is URI
+                                                                                               if (altName.get(1).equals(keyInfo[k].itemKeyName(l).getKeyName())) {
+                                                                                                       log.debug("Matched against SubjectAltName.");
+                                                                                                       return true;
+                                                                                               }
+                                                                                       }
+                                                                               }
+                                                                       }
+                                                               } catch (CertificateParsingException e1) {
+                                                                       log
+                                                                                       .error("Encountered an problem trying to extract Subject Alternate Name from supplied certificate: "
+                                                                                                       + e1);
+                                                               }
+
+                                                               //If that doesn't work, try to match using SSL-style hostname matching
+                                                               if (ShibPOSTProfile.getHostNameFromDN(certificate.getSubjectX500Principal()).equals(
+                                                                               keyInfo[k].itemKeyName(l).getKeyName())) {
+                                                                       log.debug("Matched against hostname.");
                                                                        return true;
                                                                }
                                                                        return true;
                                                                }
+
                                                        } catch (XMLSecurityException e) {
                                                                log.error("Encountered an error reading federation metadata: " + e);
                                                        }
                                                        } catch (XMLSecurityException e) {
                                                                log.error("Encountered an error reading federation metadata: " + e);
                                                        }
@@ -626,10 +667,10 @@ public class AAServlet extends TargetFederationComponent {
                return true;
        }
 
                return true;
        }
 
-       protected String getCredentialName(HttpServletRequest req) {
+       protected X509Certificate getCredentialFromProvider(HttpServletRequest req) {
                X509Certificate[] certArray = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
                if (certArray != null && certArray.length > 0) {
                X509Certificate[] certArray = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
                if (certArray != null && certArray.length > 0) {
-                       return certArray[0].getSubjectDN().getName();
+                       return certArray[0];
                }
                return null;
        }
                }
                return null;
        }
index 0b66b1e..9420087 100755 (executable)
@@ -46,6 +46,8 @@ import java.util.Vector;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.security.auth.x500.X500Principal;
+
 import org.apache.log4j.Logger;
 import org.apache.log4j.NDC;
 import org.apache.xml.security.signature.XMLSignature;
 import org.apache.log4j.Logger;
 import org.apache.log4j.NDC;
 import org.apache.xml.security.signature.XMLSignature;
@@ -71,8 +73,8 @@ import edu.internet2.middleware.shibboleth.hs.HSRelyingParty;
  * @author Scott Cantor @created April 11, 2002
  */
 public class ShibPOSTProfile {
  * @author Scott Cantor @created April 11, 2002
  */
 public class ShibPOSTProfile {
-       
-       private Pattern regex = Pattern.compile(".*CN=([^,/]+).*");
+
+       private static Pattern  regex           = Pattern.compile(".*CN=([^,/]+).*");
 
        /** XML Signature algorithm to apply */
        protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
 
        /** XML Signature algorithm to apply */
        protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
@@ -278,20 +280,17 @@ public class ShibPOSTProfile {
 
                String issuer = null;
                if (relyingParty.isLegacyProvider()) {
 
                String issuer = null;
                if (relyingParty.isLegacyProvider()) {
+                       
                        log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
                        log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
-
                        if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
                                        || relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) {
                                throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
                        }
                        if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
                                        || relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) {
                                throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
                        }
-
-                       Matcher matches = regex.matcher(relyingParty.getIdentityProvider().getResponseSigningCredential()
-                                       .getX509Certificate().getSubjectDN().getName());
-                       if (!matches.find() || matches.group(1).equals("")) {
+                       issuer = getHostNameFromDN(relyingParty.getIdentityProvider().getResponseSigningCredential()
+                                       .getX509Certificate().getSubjectX500Principal());
+                       if (issuer == null || issuer.equals("")) {
                                throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
                                throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
-
                        }
                        }
-                       issuer = matches.group(1);
 
                } else {
                        issuer = relyingParty.getIdentityProvider().getProviderId();
 
                } else {
                        issuer = relyingParty.getIdentityProvider().getProviderId();
@@ -449,4 +448,13 @@ public class ShibPOSTProfile {
                        NDC.pop();
                }
        }
                        NDC.pop();
                }
        }
+
+       public static String getHostNameFromDN(X500Principal dn) {
+               Matcher matches = regex.matcher(dn.getName(X500Principal.RFC2253));
+               if (!matches.find() || matches.groupCount() > 1) {
+                       log.error("Unable to extract host name name from certificate subject DN.");
+                       return null;
+               }
+               return matches.group(1);
+       }
 }
 }
diff --git a/tests/edu/internet2/middleware/shibboleth/aa/DNHostNameExtractionTests.java b/tests/edu/internet2/middleware/shibboleth/aa/DNHostNameExtractionTests.java
new file mode 100644 (file)
index 0000000..0f364e7
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
+ * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met: Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution, if any, must include the following acknowledgment: "This product includes
+ * software developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2
+ * Project. Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
+ * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2,
+ * nor the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
+ * products derived from this software without specific prior written permission. For written permission, please
+ * contact shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2,
+ * UCAID, or the University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name,
+ * without prior written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
+ * NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS
+ * WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED
+ * INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package edu.internet2.middleware.shibboleth.aa;
+
+import javax.security.auth.x500.X500Principal;
+
+import junit.framework.TestCase;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
+
+/**
+ * Validation suite for hack to pull hostnames out of a subject DN.
+ * 
+ * @author Walter Hoehn(wassa@columbia.edu)
+ */
+public class DNHostNameExtractionTests extends TestCase {
+
+       //Basic
+       String  dn1     = "CN=wayf.internet2.edu,OU=TSG,O=University Corporation for Advanced Internet Development,L=Ann Arbor,ST=Michigan,C=US";
+
+       //lowercase CN
+       String  dn2     = "cn=wayf.internet2.edu,OU=TSG,O=University Corporation for Advanced Internet Development,L=Ann Arbor,ST=Michigan,C=US";
+
+       //Multiple CNs
+       String  dn4     = "CN=wayf.internet2.edu,OU=TSG, CN=foo, O=University Corporation for Advanced Internet Development,L=Ann Arbor,ST=Michigan,C=US";
+
+       public DNHostNameExtractionTests(String name) {
+               super(name);
+               BasicConfigurator.resetConfiguration();
+               BasicConfigurator.configure();
+               Logger.getRootLogger().setLevel(Level.OFF);
+       }
+
+       public static void main(String[] args) {
+               junit.textui.TestRunner.run(DNHostNameExtractionTests.class);
+               BasicConfigurator.configure();
+               Logger.getRootLogger().setLevel(Level.OFF);
+       }
+
+       protected void setUp() throws Exception {
+               super.setUp();
+
+       }
+
+       public void testBasicExtraction() {
+
+               try {
+                       assertEquals("Round-trip handle validation failed on DN.", ShibPOSTProfile
+                                       .getHostNameFromDN(new X500Principal(dn1)), "wayf.internet2.edu");
+
+               } catch (Exception e) {
+                       fail("Error in test specification: " + e.getMessage());
+               }
+       }
+
+       public void testExtractionWithLowerCaseAttrName() {
+
+               try {
+                       assertEquals("Round-trip handle validation failed on DN.", ShibPOSTProfile
+                                       .getHostNameFromDN(new X500Principal(dn2)), "wayf.internet2.edu");
+
+               } catch (Exception e) {
+                       fail("Error in test specification: " + e.getMessage());
+               }
+       }
+
+       public void testExtractionWithMultipleCNs() {
+
+               try {
+                       assertEquals("Round-trip handle validation failed on DN.", ShibPOSTProfile
+                                       .getHostNameFromDN(new X500Principal(dn1)), "wayf.internet2.edu");
+
+               } catch (Exception e) {
+                       fail("Error in test specification: " + e.getMessage());
+               }
+       }
+
+}