Closes keycloak/keycloak-private#191 Closes #33116 Signed-off-by: rmartinc <rmartinc@redhat.com> Co-authored-by: Ricardo Martin <rmartinc@redhat.com>
This commit is contained in:
parent
af5eef57bf
commit
d778a8551a
3 changed files with 130 additions and 27 deletions
|
@ -59,7 +59,6 @@ import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
import javax.xml.crypto.dsig.XMLSignature;
|
|
||||||
import javax.xml.datatype.XMLGregorianCalendar;
|
import javax.xml.datatype.XMLGregorianCalendar;
|
||||||
import javax.xml.stream.XMLEventReader;
|
import javax.xml.stream.XMLEventReader;
|
||||||
|
|
||||||
|
@ -315,7 +314,7 @@ public class AssertionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Element getSignature(Element element) {
|
protected static Element getSignature(Element element) {
|
||||||
return DocumentUtil.getDirectChildElement(element, XMLSignature.XMLNS, "Signature");
|
return XMLSignatureUtil.getSignature(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -81,15 +81,22 @@ import java.security.interfaces.DSAPublicKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import javax.xml.crypto.AlgorithmMethod;
|
import javax.xml.crypto.AlgorithmMethod;
|
||||||
|
import javax.xml.crypto.Data;
|
||||||
import javax.xml.crypto.KeySelector;
|
import javax.xml.crypto.KeySelector;
|
||||||
import javax.xml.crypto.KeySelectorException;
|
import javax.xml.crypto.KeySelectorException;
|
||||||
import javax.xml.crypto.KeySelectorResult;
|
import javax.xml.crypto.KeySelectorResult;
|
||||||
|
import javax.xml.crypto.NodeSetData;
|
||||||
|
import javax.xml.crypto.URIReferenceException;
|
||||||
import javax.xml.crypto.XMLCryptoContext;
|
import javax.xml.crypto.XMLCryptoContext;
|
||||||
import javax.xml.crypto.dom.DOMStructure;
|
import javax.xml.crypto.dom.DOMStructure;
|
||||||
import org.keycloak.rotation.KeyLocator;
|
import org.keycloak.rotation.KeyLocator;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.saml.common.util.SecurityActions;
|
import org.keycloak.saml.common.util.SecurityActions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,6 +177,52 @@ public class XMLSignatureUtil {
|
||||||
return xsf;
|
return xsf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the element that contains the signature for the passed element.
|
||||||
|
*
|
||||||
|
* @param element The element to search for the signature
|
||||||
|
* @return The signature element or null
|
||||||
|
*/
|
||||||
|
public static Element getSignature(Element element) {
|
||||||
|
Document doc = element.getOwnerDocument();
|
||||||
|
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||||
|
if (element.getAttributeNode(JBossSAMLConstants.ID.get()) != null) {
|
||||||
|
// set the saml ID to be found
|
||||||
|
element.setIdAttribute(JBossSAMLConstants.ID.get(), true);
|
||||||
|
}
|
||||||
|
KeySelector nullSelector = new KeySelector() {
|
||||||
|
@Override
|
||||||
|
public KeySelectorResult select(KeyInfo ki, KeySelector.Purpose prps, AlgorithmMethod am, XMLCryptoContext xmlcc) throws KeySelectorException {
|
||||||
|
return () -> null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < nl.getLength(); i++) {
|
||||||
|
Element signatureElement = (Element) nl.item(i);
|
||||||
|
DOMValidateContext valContext = new DOMValidateContext(nullSelector, signatureElement);
|
||||||
|
DOMStructure structure = new DOMStructure(signatureElement);
|
||||||
|
XMLSignature signature = fac.unmarshalXMLSignature(structure);
|
||||||
|
for (Reference ref : (List<Reference>) signature.getSignedInfo().getReferences()) {
|
||||||
|
try {
|
||||||
|
Data data = fac.getURIDereferencer().dereference(ref, valContext);
|
||||||
|
if (data instanceof NodeSetData) {
|
||||||
|
Iterator<Node> it = ((NodeSetData) data).iterator();
|
||||||
|
if (it.hasNext() && element.equals(it.next())) {
|
||||||
|
return signatureElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (URIReferenceException e) {
|
||||||
|
logger.trace("Invalid URI reference in signature " + ref.getURI());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (MarshalException e) {
|
||||||
|
logger.trace("Error unmarshalling signature", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this method to not include the KeyInfo in the signature
|
* Use this method to not include the KeyInfo in the signature
|
||||||
*
|
*
|
||||||
|
@ -404,7 +457,7 @@ public class XMLSignatureUtil {
|
||||||
* this way both assertions and the containing document are verified when signed.
|
* this way both assertions and the containing document are verified when signed.
|
||||||
*
|
*
|
||||||
* @param signedDoc
|
* @param signedDoc
|
||||||
* @param publicKey
|
* @param locator
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*
|
*
|
||||||
|
@ -428,39 +481,46 @@ public class XMLSignatureUtil {
|
||||||
if (locator == null)
|
if (locator == null)
|
||||||
throw logger.nullValueError("Public Key");
|
throw logger.nullValueError("Public Key");
|
||||||
|
|
||||||
int signedAssertions = 0;
|
HashSet<Node> signedNodes = new HashSet<>();
|
||||||
String assertionNameSpaceUri = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < nl.getLength(); i++) {
|
for (int i = 0; i < nl.getLength(); i++) {
|
||||||
Node signatureNode = nl.item(i);
|
Node signatureNode = nl.item(i);
|
||||||
Node parent = signatureNode.getParentNode();
|
if (!validateSingleNode(signatureNode, locator, signedNodes)) {
|
||||||
if (parent != null && JBossSAMLConstants.ASSERTION.get().equals(parent.getLocalName())) {
|
|
||||||
++signedAssertions;
|
|
||||||
if (assertionNameSpaceUri == null) {
|
|
||||||
assertionNameSpaceUri = parent.getNamespaceURI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! validateSingleNode(signatureNode, locator)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeList assertions = signedDoc.getElementsByTagNameNS(assertionNameSpaceUri, JBossSAMLConstants.ASSERTION.get());
|
|
||||||
|
|
||||||
if (signedAssertions > 0 && assertions != null && assertions.getLength() != signedAssertions) {
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("SAML Response document may contain malicious assertions. Signature validation will fail.");
|
|
||||||
}
|
|
||||||
// there are unsigned assertions mixed with signed ones
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signedNodes.contains(signedDoc.getDocumentElement())) {
|
||||||
|
logger.trace("All signatures are OK and root document is signed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NodeList assertions = signedDoc.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get());
|
||||||
|
|
||||||
|
if (assertions.getLength() > 0) {
|
||||||
|
// if document is not fully signed check if all the assertions are signed
|
||||||
|
for (int i = 0; i < assertions.getLength(); i++) {
|
||||||
|
if (!signedNodes.contains(assertions.item(i))) {
|
||||||
|
logger.debug("SAML Response document may contain malicious assertions. Signature validation will fail.");
|
||||||
|
// there are unsigned assertions mixed with signed ones
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.trace("Document not signed but all assertions are signed OK");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
|
public static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
|
||||||
|
return validateSingleNode(signatureNode, locator, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validateSingleNode(Node signatureNode, final KeyLocator locator, Set<Node> signedNodes) throws MarshalException, XMLSignatureException {
|
||||||
KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
|
KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
|
||||||
try {
|
try {
|
||||||
if (validateUsingKeySelector(signatureNode, sel)) {
|
if (validateUsingKeySelector(signatureNode, sel, signedNodes)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (sel.wasKeyLocated()) {
|
if (sel.wasKeyLocated()) {
|
||||||
|
@ -477,7 +537,7 @@ public class XMLSignatureUtil {
|
||||||
|
|
||||||
for (Key key : locator) {
|
for (Key key : locator) {
|
||||||
try {
|
try {
|
||||||
if (validateUsingKeySelector(signatureNode, KeySelector.singletonKeySelector(key))) {
|
if (validateUsingKeySelector(signatureNode, KeySelector.singletonKeySelector(key), signedNodes)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (XMLSignatureException ex) { // pass through MarshalException
|
} catch (XMLSignatureException ex) { // pass through MarshalException
|
||||||
|
@ -489,12 +549,26 @@ public class XMLSignatureUtil {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean validateUsingKeySelector(Node signatureNode, KeySelector validationKeySelector) throws XMLSignatureException, MarshalException {
|
private static boolean validateUsingKeySelector(Node signatureNode, KeySelector validationKeySelector, Set<Node> signedNodes) throws XMLSignatureException, MarshalException {
|
||||||
DOMValidateContext valContext = new DOMValidateContext(validationKeySelector, signatureNode);
|
DOMValidateContext valContext = new DOMValidateContext(validationKeySelector, signatureNode);
|
||||||
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
||||||
boolean coreValidity = signature.validate(valContext);
|
boolean coreValidity = signature.validate(valContext);
|
||||||
|
|
||||||
if (! coreValidity) {
|
if (coreValidity) {
|
||||||
|
for (Reference ref : (List<Reference>) signature.getSignedInfo().getReferences()) {
|
||||||
|
try {
|
||||||
|
Data data = fac.getURIDereferencer().dereference(ref, valContext);
|
||||||
|
if (data instanceof NodeSetData) {
|
||||||
|
Iterator<Node> it = ((NodeSetData) data).iterator();
|
||||||
|
if (it.hasNext()) {
|
||||||
|
signedNodes.add(it.next()); // add the first referenced object as signed element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (URIReferenceException e) {
|
||||||
|
// ignored as signature was ok so reference can be obtained
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
boolean sv = signature.getSignatureValue().validate(valContext);
|
boolean sv = signature.getSignatureValue().validate(valContext);
|
||||||
logger.trace("Signature validation status: " + sv);
|
logger.trace("Signature validation status: " + sv);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.testsuite.util.Matchers;
|
||||||
import org.keycloak.testsuite.util.RealmBuilder;
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
import org.keycloak.testsuite.util.RoleBuilder;
|
import org.keycloak.testsuite.util.RoleBuilder;
|
||||||
import org.keycloak.testsuite.util.RolesBuilder;
|
import org.keycloak.testsuite.util.RolesBuilder;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.testsuite.util.SamlClient.Binding;
|
import org.keycloak.testsuite.util.SamlClient.Binding;
|
||||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
|
@ -66,6 +67,7 @@ import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
||||||
|
import org.keycloak.saml.common.util.DocumentUtil;
|
||||||
import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
|
import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
|
||||||
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
|
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
|
||||||
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_PRIVATE_KEY;
|
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_PRIVATE_KEY;
|
||||||
|
@ -181,6 +183,20 @@ public class SamlSignatureTest extends AbstractAdapterTest {
|
||||||
originalSignature.appendChild(object);
|
originalSignature.appendChild(object);
|
||||||
object.appendChild(assertion);
|
object.appendChild(assertion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void noDocumentSignatureOnlyOneAssertionSignedBelowResponse(Document document){
|
||||||
|
// remove the signature for the whole response
|
||||||
|
removeDocumentSignature(document);
|
||||||
|
// move the signature from the assertion to the response level
|
||||||
|
Element assertion = (Element) document.getElementsByTagNameNS(ASSERTION_NSURI.get(), "Assertion").item(0);
|
||||||
|
Element signature = (Element) assertion.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature").item(0);
|
||||||
|
assertion.removeChild(signature);
|
||||||
|
document.getDocumentElement().appendChild(signature);
|
||||||
|
// create a second assertion without signature
|
||||||
|
Element evilAssertion = (Element) assertion.cloneNode(true);
|
||||||
|
evilAssertion.setAttribute("ID", "_evil_assertion_ID");
|
||||||
|
document.getDocumentElement().insertBefore(evilAssertion, assertion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
|
@ -321,11 +337,21 @@ public class SamlSignatureTest extends AbstractAdapterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void removeDocumentSignature(Document doc) throws DOMException {
|
||||||
|
Element responseSignature = (Element) doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature").item(0);
|
||||||
|
Assert.assertNotNull(doc.getDocumentElement().removeChild(responseSignature));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNoChange() throws Exception {
|
public void testNoChange() throws Exception {
|
||||||
testSamlResponseModifications(r -> {}, true);
|
testSamlResponseModifications(r -> {}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnlyAssertionSignature() throws Exception {
|
||||||
|
testSamlResponseModifications(SamlSignatureTest::removeDocumentSignature, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRemoveSignatures() throws Exception {
|
public void testRemoveSignatures() throws Exception {
|
||||||
testSamlResponseModifications(SamlSignatureTest::removeAllSignatures, false);
|
testSamlResponseModifications(SamlSignatureTest::removeAllSignatures, false);
|
||||||
|
@ -371,4 +397,8 @@ public class SamlSignatureTest extends AbstractAdapterTest {
|
||||||
testSamlResponseModifications(XSWHelpers::applyXSW8, false);
|
testSamlResponseModifications(XSWHelpers::applyXSW8, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoDocumentSignatureOnlyOneAssertionSignedBelowResponse() throws Exception {
|
||||||
|
testSamlResponseModifications(XSWHelpers::noDocumentSignatureOnlyOneAssertionSignedBelowResponse, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue