/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, 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.core.saml.v2;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Enumeration;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.XMLSignatureException;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
import org.picketlink.identity.federation.core.exceptions.ConfigurationException;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.constants.JBossSAMLURIConstants;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil;
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
import org.picketlink.identity.federation.core.util.JAXPValidationUtil;
import org.picketlink.identity.federation.core.util.KeyStoreUtil;
import org.picketlink.identity.federation.core.util.XMLSignatureUtil;
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeType;
import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.assertion.SubjectType;
import org.picketlink.identity.federation.saml.v2.assertion.SubjectType.STSubType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.picketlink.test.identity.federation.api.saml.v2.SignatureValidationUnitTestCase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* <p>
* This test case demonstrates how PicketLink behaves when a XML Signature Wrapping Attack is performed using a SAML Response
* document. It also forces a successful 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 Silva</a>
*/
public class SAMLAssertionWrappingAttackTestCase {
private String keystoreLocation = "keystore/jbid_test_keystore.jks";
private String keystorePass = "store123";
private String alias = "servercert";
private String keyPass = "test123";
private PublicKey publicKey;
private PrivateKey privateKey;
@Before
public void onSetup() throws Exception {
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
InputStream ksStream = tcl.getResourceAsStream(keystoreLocation);
assertNotNull("Input keystore stream is not null", ksStream);
KeyStore ks = KeyStoreUtil.getKeyStore(ksStream, keystorePass.toCharArray());
assertNotNull("KeyStore is not null", ks);
// Check that there are aliases in the keystore
Enumeration<String> aliases = ks.aliases();
assertTrue("Aliases are not empty", aliases.hasMoreElements());
this.publicKey = KeyStoreUtil.getPublicKey(ks, alias, keyPass.toCharArray());
assertNotNull("Public Key is not null", publicKey);
this.privateKey = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
}
/**
* <p>
* Tests if PicketLink is blinded for XML Signature Wrapping Attacks when using a SAML Response. In this case an exception
* should be throw because the ID used to reference the signed Response will not be found. This tests shows how PicketLink
* reacts when a XML Signature Wrapping Attack is performed.
* </p>
*
* @throws Exception
*/
@Test(expected = XMLSignatureException.class)
public void testWrappingAttack() throws Exception {
ResponseType responseType = createSignedResponse();
SAML2Signature ss = new SAML2Signature();
ss.setSignatureMethod(SignatureMethod.RSA_SHA1);
Document signedDoc = ss.sign(responseType, new KeyPair(publicKey, privateKey));
Logger.getLogger(SignatureValidationUnitTestCase.class).debug(DocumentUtil.asString(signedDoc));
JAXPValidationUtil.validate(DocumentUtil.getNodeAsStream(signedDoc));
// Validate the signature
boolean isValid = XMLSignatureUtil.validate(signedDoc, publicKey);
assertTrue(isValid);
// 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) signedDoc.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) signedDoc.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);
// let's wrap the forged assertion into the original document.
Element element = evilAssertion.getDocumentElement();
Node adoptNode = signedDoc.adoptNode(element);
signedDoc.getDocumentElement().appendChild(adoptNode);
// let's append the cloned response document as a child of the original Signature element
Element signatureOriginal = (Element) signedDoc.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
signedDoc.getDocumentElement().setAttribute("ID", "evilAssertion");
// validate the original response with the wrapped assertion
isValid = XMLSignatureUtil.validate(signedDoc, publicKey);
assertTrue(false);
}
/**
* <p>
* Forces the XML Signature Wrapping Attack. This test creates a valid SAML Response properly signed.
* </p>
*
* @throws Exception
*/
@Test
public void testForceWrappingAttack() throws Exception {
ResponseType responseType = createSignedResponse();
SAML2Signature ss = new SAML2Signature();
ss.setSignatureMethod(SignatureMethod.RSA_SHA1);
Document signedDoc = ss.sign(responseType, new KeyPair(publicKey, privateKey));
Logger.getLogger(SignatureValidationUnitTestCase.class).debug(DocumentUtil.asString(signedDoc));
JAXPValidationUtil.validate(DocumentUtil.getNodeAsStream(signedDoc));
// Validate the signature
boolean isValid = XMLSignatureUtil.validate(signedDoc, publicKey);
assertTrue(isValid);
// 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) signedDoc.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) signedDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion",
"Assertion").item(0);
originalAssertion.getParentNode().removeChild(originalAssertion);
// let's load a forged assertion
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
String fileName = "saml2-wrapping-attack.xml";
InputStream is = tcl.getResourceAsStream(fileName);
Document evilAssertion = DocumentUtil.getDocument(is);
// let's wrap the forged assertion into the original document.
Element element = evilAssertion.getDocumentElement();
Node adoptNode = signedDoc.adoptNode(element);
signedDoc.getDocumentElement().appendChild(adoptNode);
// let's append the cloned response document as a child of the original Signature element
Element signatureOriginal = (Element) signedDoc.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
signedDoc.getDocumentElement().setAttribute("ID", "evilAssertion");
// let's set the idness of the ID attribute. This should force the signature validation to be successful. The two lines
// bellow are responsible to allow the attack.
importedClonedResponse = (Element) signatureOriginal.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol",
"Response").item(0);
importedClonedResponse.setIdAttribute("ID", true);
// validate the original response with the wrapped assertion
isValid = XMLSignatureUtil.validate(signedDoc, publicKey);
assertTrue(isValid);
}
private ResponseType createSignedResponse() throws ConfigurationException {
IssuerInfoHolder issuerInfo = new IssuerInfoHolder("testIssuer");
String id = IDGenerator.create("ID_");
SAML2Response response = new SAML2Response();
String authnContextDeclRef = JBossSAMLURIConstants.AC_PASSWORD_PROTECTED_TRANSPORT.get();
AuthnStatementType authnStatement = response.createAuthnStatement(authnContextDeclRef, XMLTimeUtil.getIssueInstant());
// Create an assertion
AssertionType assertion = response.createAssertion(id, issuerInfo.getIssuer());
SubjectType subject = new SubjectType();
subject.setSubType(new STSubType());
NameIDType nameId = new NameIDType();
nameId.setValue("jduke");
subject.getSubType().addBaseID(nameId);
assertion.setSubject(subject);
assertion.addStatement(authnStatement);
AttributeStatementType attributes = new AttributeStatementType();
AttributeType attribute = new AttributeType("Role");
attribute.addAttributeValue("Manager");
attributes.addAttribute(new ASTChoiceType(attribute));
assertion.addStatement(attributes);
id = IDGenerator.create("ID_"); // regenerate
return response.createResponseType(id, issuerInfo, assertion);
}
}