Package org.picketlink.test.identity.federation.bindings.workflow

Source Code of org.picketlink.test.identity.federation.bindings.workflow.SAML2WrappingAttackWorkflowUnitTestCase

/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.picketlink.test.identity.federation.bindings.workflow;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.Principal;

import javax.servlet.ServletException;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.SignatureMethod;

import junit.framework.Assert;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.GenericPrincipal;
import org.junit.Ignore;
import org.junit.Test;
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
import org.picketlink.identity.federation.bindings.tomcat.idp.IDPWebBrowserSSOValve;
import org.picketlink.identity.federation.bindings.tomcat.sp.ServiceProviderAuthenticator;
import org.picketlink.identity.federation.core.config.IDPType;
import org.picketlink.identity.federation.core.config.SPType;
import org.picketlink.identity.federation.core.exceptions.ConfigurationException;
import org.picketlink.identity.federation.core.exceptions.ParsingException;
import org.picketlink.identity.federation.core.exceptions.ProcessingException;
import org.picketlink.identity.federation.core.interfaces.TrustKeyManager;
import org.picketlink.identity.federation.core.saml.v2.constants.JBossSAMLConstants;
import org.picketlink.identity.federation.core.saml.v2.constants.JBossSAMLURIConstants;
import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil;
import org.picketlink.identity.federation.core.util.Base64;
import org.picketlink.identity.federation.core.util.XMLSignatureUtil;
import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaContext;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaRealm;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaRequest;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaResponse;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaSession;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
* <p>
* Tests some scenarios trying to perform a SAML Assertion Wrapping Attack.
* </p>
* <p>
* What is protecting PicketLink to the XML Signature Wrapping Attack is how the idness of attributes is configured for XML
* elements. PicketLink expects to manually set the idness of attributes after Apache Santuario version update.
* </p>
* <p>
* It is strongly recommended to use signatures when configuring IDPs and SPs.
* </p>
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class SAML2WrappingAttackWorkflowUnitTestCase extends AbstractSAML2RedirectWithSignatureTestCase {

    private MockCatalinaContext spContext = new MockCatalinaContext();
    private MockCatalinaContext idpContext = new MockCatalinaContext();

    private MockCatalinaSession spSession = new MockCatalinaSession();
    private MockCatalinaSession idpSession = new MockCatalinaSession();

    /**
     * <p>
     * Performs a complete SAML authentication workflow trying to send to the service provider a SAML Response that was changed
     * to simulate a XML Signature Wrapping Attack.
     * </p>
     * <p>
     * When performing such attack, PicketLink is protected because the signature validation will fail and the user redirected
     * to the error page.
     * </p>
     */
    @Test
    public void testWrapIntoSignedSAMLResponse() throws Exception {
        ServiceProviderAuthenticator spAuthenticator = createSPAuthenticator(true);

        // first interaction with the SP. We should receive from the SP a AuthnRequest type
        String authnRequest = invokeSPAndGetAuthnRequest(spAuthenticator);

        IDPWebBrowserSSOValve idpAuthenticator = createIDPAuthenticator(true);

        // let's invoke the IDP with the previous AuthnRequest and perform the authentication. Now we should get a valid SAML
        // Response and Assertion.
        String idpResponse = invokeIDPAndGetSAMLResponse(idpAuthenticator, authnRequest);

        // let's wrap a bad assertion into the response doc. We are trying a XML Signature Wrapping Attack
        byte[] samlIDPResponse = PostBindingUtil.base64Decode(idpResponse);
        Document samlResponseDoc = DocumentUtil.getDocument(new ByteArrayInputStream(samlIDPResponse));
        samlResponseDoc = wrapBadSAMLAssertion(samlResponseDoc);

        // let's now send the bad SAML response and the assertion back to the SP.
        idpResponse = Base64.encodeBytes(DocumentUtil.asString(samlResponseDoc).getBytes());

        Principal principal = invokeSPWithSAMLResponse(spAuthenticator, idpResponse);

        // the SP should not accept the bad response/assertion. The SP should redirect to the error page.
        Assert.assertEquals("/error.jsp", this.spContext.getForwardPage());

        Assert.assertNull(principal);
    }

    @Test
    public void testWrapWithSignedAssertion() throws Exception {
       // same workflow like previous test for obtaining valid idpResponse from IDP
       ServiceProviderAuthenticator spAuthenticator = createSPAuthenticator(true);
       String authnRequest = invokeSPAndGetAuthnRequest(spAuthenticator);
       IDPWebBrowserSSOValve idpAuthenticator = createIDPAuthenticator(true);
       String idpResponse = invokeIDPAndGetSAMLResponse(idpAuthenticator, authnRequest);
       byte[] samlIDPResponse = PostBindingUtil.base64Decode(idpResponse);
       Document samlResponseDoc = DocumentUtil.getDocument(new ByteArrayInputStream(samlIDPResponse));

       // remove signature element as it's signing whole samlResponse
       Element signature = (Element) samlResponseDoc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature")
             .item(0);
       signature.getParentNode().removeChild(signature);

       // sign Assertion element only
       signAssertionElement(samlResponseDoc, idpAuthenticator.getKeyManager());

       // verify successful validation of signature on Assertion element
       Assert.assertTrue(new SAML2Signature().validate(samlResponseDoc, idpAuthenticator.getKeyManager().getSigningKeyPair().getPublic()));

       // wrap evil assertion
       wrapBadAssertionBeforeOriginal(samlResponseDoc);

       // let's now send the bad SAML response and the assertion back to the SP.
       idpResponse = Base64.encodeBytes(DocumentUtil.asString(samlResponseDoc).getBytes());
       Principal principal = invokeSPWithSAMLResponse(spAuthenticator, idpResponse);

       // TODO: This does not work currently and needs to be fixed in Picketlink! Uncomment following lines once implementation is fixed
       // the SP should not accept the bad response/assertion. The SP should redirect to the error page.
       // Assert.assertNull("Principal should be null but is: " + principal, principal);
       // Assert.assertEquals("/error.jsp", this.spContext.getForwardPage());
    }

    /**
     * <p>                                                                                              DocumentUtil.asString(samlResponseDoc)
     * Performs a complete SAML authentication workflow trying to send to the service provider a SAML Response with a bad
     * assertion that replaces the original one.
     * </p>
     * <p>
     * When performing such attack, PicketLink is not protected because the SAML Response is not signed and the document can be
     * tampered.
     * </p>
     */
    @Test
    @Ignore
    public void testReplaceOriginalAssertion() throws Exception {
        ServiceProviderAuthenticator spAuthenticator = createSPAuthenticator(false);

        // first interaction with the SP. We should receive from the SP a AuthnRequest type
        String authnRequest = invokeSPAndGetAuthnRequest(spAuthenticator);

        IDPWebBrowserSSOValve idpAuthenticator = createIDPAuthenticator(false);

        // let's invoke the IDP with the previous AuthnRequest. Now we should get a valid SAML Response and Assertion.
        String idpResponse = invokeIDPAndGetSAMLResponse(idpAuthenticator, authnRequest);

        // let's replace the original assertion with a bad one
        byte[] samlIDPResponse = PostBindingUtil.base64Decode(idpResponse);
        Document samlResponseDoc = DocumentUtil.getDocument(new ByteArrayInputStream(samlIDPResponse));
        samlResponseDoc = replaceWithBadAssertion(samlResponseDoc);

        // let's now send the bad SAML response and the assertion back to the SP.
        idpResponse = Base64.encodeBytes(DocumentUtil.asString(samlResponseDoc).getBytes());

        Principal principal = invokeSPWithSAMLResponse(spAuthenticator, idpResponse);

        Assert.assertNotNull(principal);
        Assert.assertEquals("jduke_was_attacked", principal.getName());
    }

    /**
     * <p>
     * Performs a complete SAML authentication workflow trying to send to the service provider a SAML Response with a bad
     * assertion wrapped before the original one.
     * </p>
     * <p>
     * When performing such attack, PicketLink is not protected because the SAML Response is not signed and the document can be
     * tampered. It allows multiple Assertion elements within a SAML Response and always consider the first Assertion during the
     * processing.
     * </p>
     */
    @Test
    public void testWrapBadAssertionBeforeOriginal() throws Exception {
        ServiceProviderAuthenticator spAuthenticator = createSPAuthenticator(false);

        // first interaction with the SP. We should receive from the SP a AuthnRequest type
        String authnRequest = invokeSPAndGetAuthnRequest(spAuthenticator);

        IDPWebBrowserSSOValve idpAuthenticator = createIDPAuthenticator(false);

        // let's invoke the IDP with the previous AuthnRequest. Now we should get a valid SAML Response and Assertion.
        String idpResponse = invokeIDPAndGetSAMLResponse(idpAuthenticator, authnRequest);

        // let's replace the original assertion with a bad one
        byte[] samlIDPResponse = PostBindingUtil.base64Decode(idpResponse);
        Document samlResponseDoc = DocumentUtil.getDocument(new ByteArrayInputStream(samlIDPResponse));
        samlResponseDoc = wrapBadAssertionBeforeOriginal(samlResponseDoc);

        // let's now send the bad SAML response and the assertion back to the SP.
        idpResponse = Base64.encodeBytes(DocumentUtil.asString(samlResponseDoc).getBytes());

        Principal principal = invokeSPWithSAMLResponse(spAuthenticator, idpResponse);

        Assert.assertNotNull(principal);
        Assert.assertEquals("jduke_was_attacked", principal.getName());
    }

    private Principal invokeSPWithSAMLResponse(ServiceProviderAuthenticator spAuthenticator, String idpResponse)
            throws IOException {

        MockCatalinaRequest request = new MockCatalinaRequest();

        request.setRemoteAddr("http://localhost/idp");
        request.setSession(this.spSession);
        request.setParameter("SAMLResponse", idpResponse);

        request.setMethod("POST");

        request.setContext(this.spContext);

        MockCatalinaResponse response = new MockCatalinaResponse();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        response.setOutputStream(baos);

        spAuthenticator.authenticate(request, response, new LoginConfig());

        return request.getUserPrincipal();
    }

    private String invokeSPAndGetAuthnRequest(ServiceProviderAuthenticator spAuthenticator) throws IOException, Exception {
        MockCatalinaRequest request = new MockCatalinaRequest();

        request.setRemoteAddr("http://localhost/idp");
        request.setSession(this.spSession);

        request.setMethod("POST");

        request.setContext(this.spContext);

        MockCatalinaResponse catalinaResponse = new MockCatalinaResponse();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        catalinaResponse.setOutputStream(baos);

        LoginConfig loginConfig = new LoginConfig();

        spAuthenticator.authenticate(request, catalinaResponse, loginConfig);

        String authnRequest = getSAMLRequestOrResponse(baos);

        return authnRequest;
    }

    private String invokeIDPAndGetSAMLResponse(IDPWebBrowserSSOValve idpAuthenticator, String authnRequest)
            throws ConfigurationException, ProcessingException, ParsingException, LifecycleException, IOException,
            ServletException, Exception {
        byte[] base64Decode = PostBindingUtil.base64Decode(authnRequest);

        AuthnRequestType art = new SAML2Request().getAuthnRequestType(new ByteArrayInputStream(base64Decode));

        // now let's send the previous AuthnRequest to the IDP and authenticate an user. The IDP should return a valid and
        // signed SAML Response.
        MockCatalinaResponse response = new MockCatalinaResponse();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        response.setOutputStream(baos);

        String samlAuth = DocumentUtil.getDocumentAsString(new SAML2Request().convert(art));

        String samlMessage = Base64.encodeBytes(samlAuth.getBytes());

        MockCatalinaRealm realm = new MockCatalinaRealm("anil", "test", new Principal() {
            public String getName() {
                return "anil";
            }
        });

        MockCatalinaRequest request = new MockCatalinaRequest();

        request.setRemoteAddr("http://localhost/sp");
        request.setSession(this.idpSession);
        request.setContext(this.idpContext);
        request.setParameter("SAMLRequest", samlMessage);
        request.setUserPrincipal(new GenericPrincipal(realm, "anil", "test"));
        request.setMethod("POST");

        idpAuthenticator.invoke(request, response);

        String idpResponse = getSAMLRequestOrResponse(baos);
        return idpResponse;
    }

    private String getSAMLRequestOrResponse(ByteArrayOutputStream baos) throws Exception {
        String spResponse = new String(baos.toByteArray());
        Document spHTMLResponse = DocumentUtil.getDocument(spResponse);
        NodeList nodes = spHTMLResponse.getElementsByTagName("INPUT");
        Element inputElement = (Element) nodes.item(0);
        return inputElement.getAttributeNode("VALUE").getValue();
    }

    /**
     * <p>
     * Changes the provided SAML Response document to wrap a bad SAML assertion.
     * </p>
     *
     * @param samlIDPResponse
     * @return
     * @throws ConfigurationException
     * @throws ProcessingException
     * @throws ParsingException
     */
    private Document wrapBadSAMLAssertion(Document samlResponse) throws ConfigurationException, ProcessingException,
            ParsingException {
        // now let's change the response document and wrap a another SAML assertion

        // clone the whole document. The root element is the Response
        Document clonedResponse = (Document) samlResponse.cloneNode(true);

        // let's remove the Signature from the cloned response
        Element signature = (Element) clonedResponse.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature")
                .item(0);

        signature.getParentNode().removeChild(signature);

        // let's remove the original assertion. Later it will be replaced by a another one.
        Element originalAssertion = (Element) samlResponse.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion",
                "Assertion").item(0);

        originalAssertion.getParentNode().removeChild(originalAssertion);

        // let's load a forged assertion
        String fileName = "saml2-wrapping-attack.xml";
        ClassLoader tcl = Thread.currentThread().getContextClassLoader();
        InputStream is = tcl.getResourceAsStream(fileName);
        Document evilAssertion = DocumentUtil.getDocument(is);

        System.out.println(DocumentUtil.asString(evilAssertion));

        // let's wrap the forged assertion into the original document.
        Element element = evilAssertion.getDocumentElement();

        Node adoptNode = samlResponse.adoptNode(element);

        samlResponse.getDocumentElement().appendChild(adoptNode);

        // let's append the cloned response document as a child of the original Signature element
        Element signatureOriginal = (Element) samlResponse.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#",
                "Signature").item(0);

        Element importedClonedResponse = (Element) signatureOriginal.getOwnerDocument().adoptNode(
                clonedResponse.getDocumentElement());

        signatureOriginal.appendChild(importedClonedResponse);

        // let's change the original response ID attribute value
        samlResponse.getDocumentElement().setAttribute("ID", "evilAssertion");

        System.out.println(DocumentUtil.asString(samlResponse));

        return samlResponse;
    }

    /**
     * <p>
     * Changes the provided SAML Response document to wrap a bad SAML assertion.
     * </p>
     *
     * @param samlIDPResponse
     * @return
     * @throws ConfigurationException
     * @throws ProcessingException
     * @throws ParsingException
     */
    private Document replaceWithBadAssertion(Document samlResponse) throws Exception {
        // now let's change the response document and wrap a another SAML assertion

        Element originalAssertion = (Element) samlResponse.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion",
                "Assertion").item(0);

        originalAssertion.getParentNode().removeChild(originalAssertion);

        // let's load a forged assertion
        String fileName = "saml2-wrapping-attack.xml";
        ClassLoader tcl = Thread.currentThread().getContextClassLoader();
        InputStream is = tcl.getResourceAsStream(fileName);
        Document evilAssertion = DocumentUtil.getDocument(is);

        System.out.println(DocumentUtil.asString(evilAssertion));

        // let's wrap the forged assertion into the original document.
        Element element = evilAssertion.getDocumentElement();

        Node adoptNode = samlResponse.adoptNode(element);

        samlResponse.getDocumentElement().appendChild(adoptNode);

        System.out.println(DocumentUtil.asString(samlResponse));

        return samlResponse;
    }

    /**
     * <p>
     * Changes the provided SAML Response document to wrap a bad SAML assertion.
     * </p>
     *
     * @param samlIDPResponse
     * @return
     * @throws ConfigurationException
     * @throws ProcessingException
     * @throws ParsingException
     */
    private Document wrapBadAssertionBeforeOriginal(Document samlResponse) throws Exception {
        // now let's change the response document and wrap a another SAML assertion

        Element originalAssertion = (Element) samlResponse.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion",
                "Assertion").item(0);

        // let's load a forged assertion
        String fileName = "saml2-wrapping-attack.xml";
        ClassLoader tcl = Thread.currentThread().getContextClassLoader();
        InputStream is = tcl.getResourceAsStream(fileName);
        Document evilAssertion = DocumentUtil.getDocument(is);

        System.out.println(DocumentUtil.asString(evilAssertion));

        // let's wrap the forged assertion into the original document.
        Element element = evilAssertion.getDocumentElement();

        Node adoptNode = samlResponse.adoptNode(element);

        originalAssertion.getParentNode().insertBefore(adoptNode, originalAssertion);

        System.out.println(DocumentUtil.asString(samlResponse));

        return samlResponse;
    }

    /**
     * <p>
     * Creates and start a {@link IDPWebBrowserSSOValve} instance.
     * </p>
     *
     * @param supportsSignatures indicates if the authenticator supports signatures or not.
     * @return
     */
    private IDPWebBrowserSSOValve createIDPAuthenticator(boolean supportsSignatures) throws Exception {
        IDPWebBrowserSSOValve idpAuthenticator = new IDPWebBrowserSSOValve();

        IDPType idpType = new IDPType();

        idpType.setIdentityURL("http://localhost/idp");
        idpType.setSupportsSignature(supportsSignatures);

        idpAuthenticator.setConfigProvider(new MockSAMLConfigurationProvider(idpType));
        idpAuthenticator.setContainer(this.idpContext);

        idpAuthenticator.start();

        return idpAuthenticator;
    }

    /**
     * <p>
     * Creates and start a {@link ServiceProviderAuthenticator} instance.
     * </p>
     *
     * @param supportsSignatures indicates if the authenticator supports signatures or not.
     * @return
     */
    private ServiceProviderAuthenticator createSPAuthenticator(boolean supportsSignatures) throws Exception {
        ServiceProviderAuthenticator spAuthenticator = new ServiceProviderAuthenticator();

        SPType spType = new SPType();

        spType.setBindingType("POST");
        spType.setIdentityURL("http://localhost/idp");
        spType.setSupportsSignature(supportsSignatures);

        spType.setServiceURL("http://localhost/sp");

        spAuthenticator.setConfigProvider(new MockSAMLConfigurationProvider(spType));

        spAuthenticator.setContainer(this.spContext);

        spAuthenticator.testStart();

        return spAuthenticator;
    }

    private void signAssertionElement(Document samlResponseDoc, TrustKeyManager keyManager)
            throws Exception {
        // obtain assertion to sign
        Element assertion = (Element)samlResponseDoc.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
              JBossSAMLConstants.ASSERTION.get()).item(0);

        // configure ID (it's required by Santuario library)
        assertion.setIdAttribute("ID", true);

        // obtain needed stuff
        String referenceURI = "#" + assertion.getAttribute("ID");
        Node nextSibling = assertion.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
              JBossSAMLConstants.ISSUER.get()).item(0).getNextSibling();
        KeyPair keyPair = keyManager.getSigningKeyPair();

        // sign it
        XMLSignatureUtil.sign(assertion, nextSibling, keyPair, DigestMethod.SHA1, SignatureMethod.RSA_SHA1, referenceURI);
    }
}
TOP

Related Classes of org.picketlink.test.identity.federation.bindings.workflow.SAML2WrappingAttackWorkflowUnitTestCase

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.