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