diff --git a/adapters/saml/core/nbproject/project.properties b/adapters/saml/core/nbproject/project.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml
index 16dce33f2c..b01061b396 100755
--- a/adapters/saml/core/pom.xml
+++ b/adapters/saml/core/pom.xml
@@ -34,6 +34,7 @@
${maven.build.timestamp}
yyyy-MM-dd HH:mm
+
org.keycloak
@@ -70,6 +71,11 @@
junit
test
+
+ org.apache.httpcomponents
+ httpclient
+ provided
+
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
index 693e06ebac..6ddf52c693 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
@@ -80,6 +80,8 @@ public abstract class AbstractInitiateLogin implements AuthChallenge {
}
binding.signWith(null, keypair);
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // .addExtension(new KeycloakKeySamlExtensionGenerator());
binding.signDocument();
}
return binding;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
index ee753ade11..fcbe1e9845 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
@@ -24,6 +24,12 @@ import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Set;
+import org.apache.http.client.HttpClient;
+import org.keycloak.adapters.HttpClientBuilder;
+import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator;
+import org.keycloak.rotation.CompositeKeyLocator;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
/**
* @author Bill Burke
@@ -179,10 +185,14 @@ public class DefaultSamlDeployment implements SamlDeployment {
public static class DefaultIDP implements IDP {
+ private static final int DEFAULT_CACHE_TTL = 24 * 60 * 60;
+
private String entityID;
- private PublicKey signatureValidationKey;
+ private final CompositeKeyLocator signatureValidationKeyLocator = new CompositeKeyLocator();
private SingleSignOnService singleSignOnService;
private SingleLogoutService singleLogoutService;
+ private HardcodedKeyLocator hardcodedKeyLocator;
+ private int minTimeBetweenDescriptorRequests;
@Override
public String getEntityID() {
@@ -200,8 +210,17 @@ public class DefaultSamlDeployment implements SamlDeployment {
}
@Override
- public PublicKey getSignatureValidationKey() {
- return signatureValidationKey;
+ public KeyLocator getSignatureValidationKeyLocator() {
+ return this.signatureValidationKeyLocator;
+ }
+
+ @Override
+ public int getMinTimeBetweenDescriptorRequests() {
+ return minTimeBetweenDescriptorRequests;
+ }
+
+ public void setMinTimeBetweenDescriptorRequests(int minTimeBetweenDescriptorRequests) {
+ this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests;
}
public void setEntityID(String entityID) {
@@ -209,16 +228,35 @@ public class DefaultSamlDeployment implements SamlDeployment {
}
public void setSignatureValidationKey(PublicKey signatureValidationKey) {
- this.signatureValidationKey = signatureValidationKey;
+ this.hardcodedKeyLocator = signatureValidationKey == null ? null : new HardcodedKeyLocator(signatureValidationKey);
+ refreshKeyLocatorConfiguration();
}
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
this.singleSignOnService = singleSignOnService;
+ refreshKeyLocatorConfiguration();
}
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
this.singleLogoutService = singleLogoutService;
}
+
+ public void refreshKeyLocatorConfiguration() {
+ this.signatureValidationKeyLocator.clear();
+
+ // When key is set, use that (and only that), otherwise configure dynamic key locator
+ if (this.hardcodedKeyLocator != null) {
+ this.signatureValidationKeyLocator.add(this.hardcodedKeyLocator);
+ } else if (this.singleSignOnService != null) {
+ String samlDescriptorUrl = singleSignOnService.getRequestBindingUrl() + "/descriptor";
+ // TODO
+ HttpClient httpClient = new HttpClientBuilder().build();
+ SamlDescriptorPublicKeyLocator samlDescriptorPublicKeyLocator =
+ new SamlDescriptorPublicKeyLocator(
+ samlDescriptorUrl, this.minTimeBetweenDescriptorRequests, DEFAULT_CACHE_TTL, httpClient);
+ this.signatureValidationKeyLocator.add(samlDescriptorPublicKeyLocator);
+ }
+ }
}
private IDP idp;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
index 0b82ff24cd..f01b6a1cc8 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
@@ -22,14 +22,17 @@ import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair;
import java.security.PrivateKey;
-import java.security.PublicKey;
import java.util.Set;
+import org.keycloak.rotation.KeyLocator;
/**
+ * Represents SAML deployment configuration.
+ *
* @author Bill Burke
* @version $Revision: 1 $
*/
public interface SamlDeployment {
+
enum Binding {
POST,
REDIRECT;
@@ -41,20 +44,62 @@ public interface SamlDeployment {
}
public interface IDP {
+ /**
+ * Returns entity identifier of this IdP.
+ * @return see description.
+ */
String getEntityID();
+ /**
+ * Returns Single sign on service configuration for this IdP.
+ * @return see description.
+ */
SingleSignOnService getSingleSignOnService();
+
+ /**
+ * Returns Single logout service configuration for this IdP.
+ * @return see description.
+ */
SingleLogoutService getSingleLogoutService();
- PublicKey getSignatureValidationKey();
+
+ /**
+ * Returns {@link KeyLocator} looking up public keys used for validation of IdP signatures.
+ * @return see description.
+ */
+ KeyLocator getSignatureValidationKeyLocator();
+
+ /**
+ * Returns minimum time (in seconds) between issuing requests to IdP SAML descriptor.
+ * Used e.g. by {@link KeyLocator} looking up public keys for validation of IdP signatures
+ * to prevent too frequent requests.
+ *
+ * @return see description.
+ */
+ int getMinTimeBetweenDescriptorRequests();
public interface SingleSignOnService {
+ /**
+ * Returns {@code true} if the requests to IdP need to be signed by SP key.
+ * @return see dscription
+ */
boolean signRequest();
+ /**
+ * Returns {@code true} if the complete response message from IdP should
+ * be checked for valid signature.
+ * @return see dscription
+ */
boolean validateResponseSignature();
+ /**
+ * Returns {@code true} if individual assertions in response from IdP should
+ * be checked for valid signature.
+ * @return see dscription
+ */
boolean validateAssertionSignature();
Binding getRequestBinding();
Binding getResponseBinding();
String getRequestBindingUrl();
}
+
public interface SingleLogoutService {
boolean validateRequestSignature();
boolean validateResponseSignature();
@@ -67,10 +112,19 @@ public interface SamlDeployment {
}
}
+ /**
+ * Returns Identity Provider configuration for this SAML deployment.
+ * @return see description.
+ */
public IDP getIDP();
public boolean isConfigured();
SslRequired getSslRequired();
+
+ /**
+ * Returns entity identifier of this SP.
+ * @return see description.
+ */
String getEntityID();
String getNameIDPolicyFormat();
boolean isForceAuthentication();
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
index d6e4bce41a..ee21620bd6 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
@@ -202,6 +202,7 @@ public class DeploymentBuilder {
}
}
+ idp.refreshKeyLocatorConfiguration();
return deployment;
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index e9247b3bbe..429d610dd4 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -64,11 +64,20 @@ import org.w3c.dom.Node;
import java.io.IOException;
import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyManagementException;
import java.security.PublicKey;
import java.security.Signature;
+import java.security.SignatureException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.w3c.dom.Element;
/**
*
@@ -257,13 +266,44 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
+ KeyLocator signatureValidationKey = deployment.getIDP().getSignatureValidationKeyLocator();
if (postBinding) {
- verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
+ verifyPostBindingSignature(holder.getSamlDocument(), signatureValidationKey);
} else {
- verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
+ String keyId = getMessageSigningKeyId(holder.getSamlObject());
+ verifyRedirectBindingSignature(paramKey, signatureValidationKey, keyId);
}
}
+ private String getMessageSigningKeyId(SAML2Object doc) {
+ final ExtensionsType extensions;
+ if (doc instanceof RequestAbstractType) {
+ extensions = ((RequestAbstractType) doc).getExtensions();
+ } else if (doc instanceof StatusResponseType) {
+ extensions = ((StatusResponseType) doc).getExtensions();
+ } else {
+ return null;
+ }
+
+ if (extensions == null) {
+ return null;
+ }
+
+ for (Object ext : extensions.getAny()) {
+ if (! (ext instanceof Element)) {
+ continue;
+ }
+
+ String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
+
+ if (res != null) {
+ return res;
+ }
+ }
+
+ return null;
+ }
+
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
if(statusCode != null && statusCode.getValue()!=null){
String v = statusCode.getValue().toString();
@@ -473,10 +513,10 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return false;
}
- public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
+ public void verifyPostBindingSignature(Document document, KeyLocator keyLocator) throws VerificationException {
SAML2Signature saml2Signature = new SAML2Signature();
try {
- if (!saml2Signature.validate(document, publicKey)) {
+ if (!saml2Signature.validate(document, keyLocator)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
@@ -484,7 +524,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
}
- public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
+ private void verifyRedirectBindingSignature(String paramKey, KeyLocator keyLocator, String keyId) throws VerificationException {
String request = facade.getRequest().getQueryParamValue(paramKey);
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
@@ -511,16 +551,80 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
try {
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
byte[] decodedSignature = Base64.decode(signature);
+ byte[] rawQueryBytes = rawQuery.getBytes("UTF-8");
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
- Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
- validator.initVerify(publicKey);
- validator.update(rawQuery.getBytes("UTF-8"));
- if (!validator.verify(decodedSignature)) {
+
+ if (! validateRedirectBindingSignature(signatureAlgorithm, rawQueryBytes, decodedSignature, keyLocator, keyId)) {
throw new VerificationException("Invalid query param signature");
}
} catch (Exception e) {
throw new VerificationException(e);
}
}
+
+ private boolean validateRedirectBindingSignature(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, KeyLocator locator, String keyId)
+ throws KeyManagementException, VerificationException {
+ try {
+ Key key;
+ try {
+ key = locator.getKey(keyId);
+ boolean keyLocated = key != null;
+
+ if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
+ return true;
+ }
+
+ if (keyLocated) {
+ return false;
+ }
+ } catch (KeyManagementException ex) {
+ }
+ } catch (SignatureException ex) {
+ log.debug("Verification failed for key %s: %s", keyId, ex);
+ log.trace(ex);
+ }
+
+ if (locator instanceof Iterable) {
+ Iterable availableKeys = (Iterable) locator;
+
+ log.trace("Trying hard to validate XML signature using all available keys.");
+
+ for (Key key : availableKeys) {
+ try {
+ if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
+ return true;
+ }
+ } catch (SignatureException ex) {
+ log.debug("Verification failed: %s", ex);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private boolean validateRedirectBindingSignatureForKey(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, Key key)
+ throws SignatureException {
+ if (key == null) {
+ return false;
+ }
+
+ if (! (key instanceof PublicKey)) {
+ log.warnf("Unusable key for signature validation: %s", key);
+ return false;
+ }
+
+ Signature signature = sigAlg.createSignature(); // todo plugin signature alg
+ try {
+ signature.initVerify((PublicKey) key);
+ } catch (InvalidKeyException ex) {
+ log.warnf(ex, "Unusable key for signature validation: %s", key);
+ return false;
+ }
+
+ signature.update(rawQueryBytes);
+
+ return signature.verify(decodedSignature);
+ }
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
index 358135775d..231c425337 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
@@ -84,6 +84,8 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
.signWith(null, deployment.getSigningKeyPair())
.signDocument();
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // .addExtension(new KeycloakKeySamlExtensionGenerator());
}
@@ -115,6 +117,8 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
binding.signatureAlgorithm(deployment.getSignatureAlgorithm());
binding.signWith(null, deployment.getSigningKeyPair())
.signDocument();
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // .addExtension(new KeycloakKeySamlExtensionGenerator());
}
binding.relayState("logout");
diff --git a/saml-core/nbproject/project.properties b/saml-core/nbproject/project.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
index 78a448051e..f820a5ece4 100755
--- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
@@ -38,11 +38,14 @@ import javax.crypto.spec.SecretKeySpec;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.namespace.QName;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.URI;
+import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
+import java.security.SignatureException;
import java.security.cert.X509Certificate;
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
@@ -338,7 +341,7 @@ public class BaseSAML2BindingBuilder {
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
String documentAsString = DocumentUtil.getDocumentAsString(document);
- logger.debugv("saml docment: {0}", documentAsString);
+ logger.debugv("saml document: {0}", documentAsString);
byte[] responseBytes = documentAsString.getBytes("UTF-8");
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
@@ -363,7 +366,7 @@ public class BaseSAML2BindingBuilder {
signature.initSign(signingKeyPair.getPrivate());
signature.update(rawQuery.getBytes("UTF-8"));
sig = signature.sign();
- } catch (Exception e) {
+ } catch (InvalidKeyException | UnsupportedEncodingException | SignatureException e) {
throw new ProcessingException(e);
}
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
index 57777ab6b0..49c8df84cc 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
@@ -35,8 +35,8 @@ import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.parsers.ParserConfigurationException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
-import java.security.PublicKey;
import java.security.cert.X509Certificate;
+import org.keycloak.rotation.KeyLocator;
/**
* Class that deals with SAML2 Signature
@@ -159,7 +159,7 @@ public class SAML2Signature {
String id = samlDocument.getDocumentElement().getAttribute(ID_ATTRIBUTE_NAME);
try {
sign(samlDocument, id, keyId, keypair, canonicalizationMethodType);
- } catch (Exception e) {
+ } catch (ParserConfigurationException | GeneralSecurityException | MarshalException | XMLSignatureException e) {
throw new ProcessingException(logger.signatureError(e));
}
}
@@ -168,20 +168,18 @@ public class SAML2Signature {
* Validate the SAML2 Document
*
* @param signedDocument
- * @param publicKey
+ * @param keyLocator
*
* @return
*
* @throws ProcessingException
*/
- public boolean validate(Document signedDocument, PublicKey publicKey) throws ProcessingException {
+ public boolean validate(Document signedDocument, KeyLocator keyLocator) throws ProcessingException {
try {
configureIdAttribute(signedDocument);
- return XMLSignatureUtil.validate(signedDocument, publicKey);
- } catch (MarshalException me) {
+ return XMLSignatureUtil.validate(signedDocument, keyLocator);
+ } catch (MarshalException | XMLSignatureException me) {
throw new ProcessingException(logger.signatureError(me));
- } catch (XMLSignatureException xse) {
- throw new ProcessingException(logger.signatureError(xse));
}
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index 67fb78f5a7..ed941a0956 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -62,6 +62,7 @@ import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import org.keycloak.rotation.HardcodedKeyLocator;
/**
* Utility to deal with assertions
@@ -276,7 +277,7 @@ public class AssertionUtil {
Node n = doc.importNode(assertionElement, true);
doc.appendChild(n);
- return new SAML2Signature().validate(doc, publicKey);
+ return new SAML2Signature().validate(doc, new HardcodedKeyLocator(publicKey));
} catch (Exception e) {
logger.signatureAssertionValidationError(e);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java
new file mode 100644
index 0000000000..1bb90ea9b4
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.saml.processing.core.util;
+
+import java.util.Objects;
+import javax.xml.stream.XMLStreamWriter;
+import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.common.util.StaxUtil;
+import org.w3c.dom.Element;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeycloakKeySamlExtensionGenerator implements SamlProtocolExtensionsAwareBuilder.NodeGenerator {
+
+ public static final String NS_URI = "urn:keycloak:ext:key:1.0";
+
+ public static final String NS_PREFIX = "kckey";
+
+ public static final String KC_KEY_INFO_ELEMENT_NAME = "KeyInfo";
+
+ public static final String KEY_ID_ATTRIBUTE_NAME = "MessageSigningKeyId";
+
+ private final String keyId;
+
+ public KeycloakKeySamlExtensionGenerator(String keyId) {
+ this.keyId = keyId;
+ }
+
+ @Override
+ public void write(XMLStreamWriter writer) throws ProcessingException {
+ StaxUtil.writeStartElement(writer, NS_PREFIX, KC_KEY_INFO_ELEMENT_NAME, NS_URI);
+ StaxUtil.writeNameSpace(writer, NS_PREFIX, NS_URI);
+ if (this.keyId != null) {
+ StaxUtil.writeAttribute(writer, KEY_ID_ATTRIBUTE_NAME, this.keyId);
+ }
+ StaxUtil.writeEndElement(writer);
+ StaxUtil.flush(writer);
+ }
+
+ /**
+ * Checks that the given element is indeed a Keycloak extension {@code KeyInfo} element and
+ * returns a content of {@code MessageSigningKeyId} attribute in the given element.
+ * @param element Element to obtain the key info from.
+ * @return {@code null} if the element is unknown or there is {@code MessageSigningKeyId} attribute unset,
+ * value of the {@code MessageSigningKeyId} attribute otherwise.
+ */
+ public static String getMessageSigningKeyIdFromElement(Element element) {
+ if (Objects.equals(element.getNamespaceURI(), NS_URI) &&
+ Objects.equals(element.getLocalName(), KC_KEY_INFO_ELEMENT_NAME) &&
+ element.hasAttribute(KEY_ID_ATTRIBUTE_NAME)) {
+ return element.getAttribute(KEY_ID_ATTRIBUTE_NAME);
+ }
+
+ return null;
+ }
+
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
index 70000756ca..193af19dff 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
@@ -54,8 +54,6 @@ import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
-import javax.xml.crypto.dsig.keyinfo.KeyValue;
-import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.namespace.QName;
@@ -69,6 +67,7 @@ import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyException;
+import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
@@ -81,7 +80,14 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import javax.xml.crypto.AlgorithmMethod;
+import javax.xml.crypto.KeySelector;
+import javax.xml.crypto.KeySelectorException;
+import javax.xml.crypto.KeySelectorResult;
+import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.keyinfo.KeyName;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.api.util.KeyInfoTools;
/**
* Utility for XML Signature Note: You can change the canonicalization method type by using the system property
@@ -107,15 +113,66 @@ public class XMLSignatureUtil {
;
- private static String canonicalizationMethodType = CanonicalizationMethod.EXCLUSIVE;
-
- private static XMLSignatureFactory fac = getXMLSignatureFactory();
+ private static final XMLSignatureFactory fac = getXMLSignatureFactory();
/**
* By default, we include the keyinfo in the signature
*/
private static boolean includeKeyInfoInSignature = true;
+ private static class KeySelectorUtilizingKeyNameHint extends KeySelector {
+
+ private final KeyLocator locator;
+
+ private boolean keyLocated = false;
+
+ private String keyName = null;
+
+ public KeySelectorUtilizingKeyNameHint(KeyLocator locator) {
+ this.locator = locator;
+ }
+
+ @Override
+ public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException {
+ try {
+ KeyName keyNameEl = KeyInfoTools.getKeyName(keyInfo);
+ this.keyName = keyNameEl == null ? null : keyNameEl.getName();
+ final Key key = locator.getKey(keyName);
+ this.keyLocated = key != null;
+ return new KeySelectorResult() {
+ @Override public Key getKey() {
+ return key;
+ }
+ };
+ } catch (KeyManagementException ex) {
+ throw new KeySelectorException(ex);
+ }
+
+ }
+
+ private boolean wasKeyLocated() {
+ return this.keyLocated;
+ }
+ }
+
+ private static class KeySelectorPresetKey extends KeySelector {
+
+ private final Key key;
+
+ public KeySelectorPresetKey(Key key) {
+ this.key = key;
+ }
+
+ @Override
+ public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) {
+ return new KeySelectorResult() {
+ @Override public Key getKey() {
+ return key;
+ }
+ };
+ }
+ }
+
private static XMLSignatureFactory getXMLSignatureFactory() {
XMLSignatureFactory xsf = null;
@@ -333,6 +390,7 @@ public class XMLSignatureUtil {
public static Document sign(SignatureUtilTransferObject dto, String canonicalizationMethodType) throws GeneralSecurityException, MarshalException,
XMLSignatureException {
Document doc = dto.getDocumentToBeSigned();
+ String keyId = dto.getKeyId();
KeyPair keyPair = dto.getKeyPair();
Node nextSibling = dto.getNextSibling();
String digestMethod = dto.getDigestMethod();
@@ -346,13 +404,14 @@ public class XMLSignatureUtil {
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement(), nextSibling);
- signImpl(dsc, digestMethod, signatureMethod, referenceURI, dto.getKeyId(), publicKey, dto.getX509Certificate(), canonicalizationMethodType);
+ signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, dto.getX509Certificate(), canonicalizationMethodType);
return doc;
}
/**
- * Validate a signed document with the given public key
+ * Validate a signed document with the given public key. All elements that contain a Signature are checked,
+ * this way both assertions and the containing document are verified when signed.
*
* @param signedDoc
* @param publicKey
@@ -363,7 +422,7 @@ public class XMLSignatureUtil {
* @throws XMLSignatureException
*/
@SuppressWarnings("unchecked")
- public static boolean validate(Document signedDoc, Key publicKey) throws MarshalException, XMLSignatureException {
+ public static boolean validate(Document signedDoc, final KeyLocator locator) throws MarshalException, XMLSignatureException {
if (signedDoc == null)
throw logger.nullArgumentError("Signed Document");
@@ -376,7 +435,7 @@ public class XMLSignatureUtil {
return false;
}
- if (publicKey == null)
+ if (locator == null)
throw logger.nullValueError("Public Key");
int signedAssertions = 0;
@@ -392,24 +451,7 @@ public class XMLSignatureUtil {
}
}
- DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(i));
- XMLSignature signature = fac.unmarshalXMLSignature(valContext);
-
- boolean coreValidity = signature.validate(valContext);
-
- if (!coreValidity) {
- if (logger.isTraceEnabled()) {
- boolean sv = signature.getSignatureValue().validate(valContext);
- logger.trace("Signature validation status: " + sv);
-
- List references = signature.getSignedInfo().getReferences();
- for (Reference ref : references) {
- logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
- }
- }
-
- return false;
- }
+ if (! validateSingleNode(signatureNode, locator)) return false;
}
NodeList assertions = signedDoc.getElementsByTagNameNS(assertionNameSpaceUri, JBossSAMLConstants.ASSERTION.get());
@@ -425,6 +467,62 @@ public class XMLSignatureUtil {
return true;
}
+ private static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
+ KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
+ try {
+ if (validateUsingKeySelector(signatureNode, sel)) {
+ return true;
+ }
+ if (sel.wasKeyLocated()) {
+ return false;
+ }
+ } catch (XMLSignatureException ex) { // pass through MarshalException
+ logger.debug("Verification failed for key " + sel.keyName + ": " + ex);
+ logger.trace(ex);
+ }
+
+ logger.trace("Could not validate signature using ds:KeyInfo/ds:KeyName hint.");
+
+ if (locator instanceof Iterable) {
+ Iterable availableKeys = (Iterable) locator;
+
+ logger.trace("Trying hard to validate XML signature using all available keys.");
+
+ for (Key key : availableKeys) {
+ try {
+ if (validateUsingKeySelector(signatureNode, new KeySelectorPresetKey(key))) {
+ return true;
+ }
+ } catch (XMLSignatureException ex) { // pass through MarshalException
+ logger.debug("Verification failed: " + ex);
+ logger.trace(ex);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean validateUsingKeySelector(Node signatureNode, KeySelector validationKeySelector) throws XMLSignatureException, MarshalException {
+ DOMValidateContext valContext = new DOMValidateContext(validationKeySelector, signatureNode);
+ XMLSignature signature = fac.unmarshalXMLSignature(valContext);
+ boolean coreValidity = signature.validate(valContext);
+
+ if (! coreValidity) {
+ if (logger.isTraceEnabled()) {
+ boolean sv = signature.getSignatureValue().validate(valContext);
+ logger.trace("Signature validation status: " + sv);
+
+ List references = signature.getSignedInfo().getReferences();
+ for (Reference ref : references) {
+ logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
+ }
+ }
+ }
+
+ return coreValidity;
+ }
+
/**
* Marshall a SignatureType to output stream
*
@@ -605,7 +703,7 @@ public class XMLSignatureUtil {
Transform transform1 = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
Transform transform2 = fac.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null);
- List transformList = new ArrayList();
+ List transformList = new ArrayList<>();
transformList.add(transform1);
transformList.add(transform2);
@@ -618,7 +716,7 @@ public class XMLSignatureUtil {
SignatureMethod signatureMethodObj = fac.newSignatureMethod(signatureMethod, null);
SignedInfo si = fac.newSignedInfo(canonicalizationMethod, signatureMethodObj, referenceList);
- KeyInfo ki = null;
+ KeyInfo ki;
if (includeKeyInfoInSignature) {
ki = createKeyInfo(keyId, publicKey, x509Certificate);
} else {
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index d88a34eac2..3ee5b93a4b 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -76,6 +76,9 @@ import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author Bill Burke
@@ -174,14 +177,17 @@ public class SAMLEndpoint {
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
- protected PublicKey getIDPKey() {
+
+ protected KeyLocator getIDPKeyLocator() {
+ // TODO !!!!!!!!!!!!!!!! Parse key from IDP's SAML descriptor
+
X509Certificate certificate = null;
try {
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
} catch (ProcessingException e) {
throw new RuntimeException(e);
}
- return certificate.getPublicKey();
+ return new HardcodedKeyLocator(certificate.getPublicKey());
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
@@ -265,14 +271,18 @@ public class SAMLEndpoint {
builder.issuer(issuerURL);
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(relayState);
+ boolean postBinding = config.isPostBindingResponse();
if (config.isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
binding.signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
.signDocument();
+ if (! postBinding) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
- if (config.isPostBindingResponse()) {
+ if (postBinding) {
return binding.postBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
} else {
return binding.redirectBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
@@ -418,7 +428,7 @@ public class SAMLEndpoint {
protected class PostBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
- SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
+ SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
}
@Override
@@ -440,8 +450,8 @@ public class SAMLEndpoint {
protected class RedirectBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
- PublicKey publicKey = getIDPKey();
- SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, key);
+ KeyLocator locator = getIDPKeyLocator();
+ SamlProtocolUtils.verifyRedirectSignature(documentHolder, locator, uriInfo, key);
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index 6452c74742..e1f8d16484 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -50,8 +50,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author Pedro Igor
@@ -97,6 +96,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProviderBill Burke
* @version $Revision: 1 $
*/
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 2b6b9f76fa..7acb155b2f 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -76,6 +76,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author Bill Burke
@@ -373,7 +374,15 @@ public class SamlProtocol implements LoginProtocol {
}
Document samlDocument = null;
+ KeyManager keyManager = session.keys();
+ KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
+ boolean postBinding = isPostBinding(clientSession);
+
try {
+ if ((! postBinding) && samlClient.requiresRealmSignature()) {
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
+
ResponseType samlModel = builder.buildModel();
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
populateRoles(roleListMapper, session, userSession, clientSession, attributeStatement);
@@ -394,9 +403,6 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder();
bindingBuilder.relayState(relayState);
- KeyManager keyManager = session.keys();
- KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
-
if (samlClient.requiresRealmSignature()) {
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
@@ -496,12 +502,17 @@ public class SamlProtocol implements LoginProtocol {
if (isLogoutPostBindingForClient(clientSession)) {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
+ // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add element
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
} else {
logger.debug("frontchannel redirect binding");
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
+ if (samlClient.requiresRealmSignature()) {
+ KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ logoutBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.redirectBinding(logoutBuilder.buildDocument()).request(bindingUri);
}
@@ -534,6 +545,7 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
binding.relayState(logoutRelayState);
String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
+ boolean postBinding = isLogoutPostBindingForInitiator(userSession);
if (signingAlgorithm != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
String canonicalization = userSession.getNote(SAML_LOGOUT_CANONICALIZATION);
@@ -542,6 +554,9 @@ public class SamlProtocol implements LoginProtocol {
}
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ if (! postBinding) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
@@ -577,6 +592,7 @@ public class SamlProtocol implements LoginProtocol {
String logoutRequestString = null;
try {
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
+ // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add element
logoutRequestString = binding.postBinding(logoutBuilder.buildDocument()).encoded();
} catch (Exception e) {
logger.warn("failed to send saml logout", e);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
index e1a7c98c0d..026a54a599 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@ -17,6 +17,7 @@
package org.keycloak.protocol.saml;
+import java.security.Key;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.models.ClientModel;
@@ -33,6 +34,15 @@ import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.w3c.dom.Element;
/**
* @author Bill Burke
@@ -40,20 +50,36 @@ import java.security.cert.Certificate;
*/
public class SamlProtocolUtils {
-
+ /**
+ * Verifies a signature of the given SAML document using settings for the given client.
+ * Throws an exception if the client signature is expected to be present as per the client
+ * settings and it is invalid, otherwise returns back to the caller.
+ *
+ * @param client
+ * @param document
+ * @throws VerificationException
+ */
public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
SamlClient samlClient = new SamlClient(client);
if (!samlClient.requiresClientSignature()) {
return;
}
PublicKey publicKey = getSignatureValidationKey(client);
- verifyDocumentSignature(document, publicKey);
+ verifyDocumentSignature(document, new HardcodedKeyLocator(publicKey));
}
- public static void verifyDocumentSignature(Document document, PublicKey publicKey) throws VerificationException {
+ /**
+ * Verifies a signature of the given SAML document using keys obtained from the given key locator.
+ * Throws an exception if the client signature is invalid, otherwise returns back to the caller.
+ *
+ * @param document
+ * @param keyLocator
+ * @throws VerificationException
+ */
+ public static void verifyDocumentSignature(Document document, KeyLocator keyLocator) throws VerificationException {
SAML2Signature saml2Signature = new SAML2Signature();
try {
- if (!saml2Signature.validate(document, publicKey)) {
+ if (!saml2Signature.validate(document, keyLocator)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
@@ -61,10 +87,22 @@ public class SamlProtocolUtils {
}
}
+ /**
+ * Returns public part of SAML signing key from the client settings.
+ * @param client
+ * @return Public key for signature validation.
+ * @throws VerificationException
+ */
public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
return getPublicKey(new SamlClient(client).getClientSigningCertificate());
}
+ /**
+ * Returns public part of SAML encryption key from the client settings.
+ * @param client
+ * @return Public key for encryption.
+ * @throws VerificationException
+ */
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
}
@@ -85,7 +123,7 @@ public class SamlProtocolUtils {
return cert.getPublicKey();
}
- public static void verifyRedirectSignature(PublicKey publicKey, UriInfo uriInformation, String paramKey) throws VerificationException {
+ public static void verifyRedirectSignature(SAMLDocumentHolder documentHolder, KeyLocator locator, UriInfo uriInformation, String paramKey) throws VerificationException {
MultivaluedMap encodedParams = uriInformation.getQueryParameters(false);
String request = encodedParams.getFirst(paramKey);
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
@@ -96,10 +134,11 @@ public class SamlProtocolUtils {
if (algorithm == null) throw new VerificationException("SigAlg was null");
if (signature == null) throw new VerificationException("Signature was null");
+ String keyId = getMessageSigningKeyId(documentHolder.getSamlObject());
+
// Shibboleth doesn't sign the document for redirect binding.
// todo maybe a flag?
-
UriBuilder builder = UriBuilder.fromPath("/")
.queryParam(paramKey, request);
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
@@ -113,8 +152,13 @@ public class SamlProtocolUtils {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
- validator.initVerify(publicKey);
- validator.update(rawQuery.getBytes("UTF-8"));
+ Key key = locator.getKey(keyId);
+ if (key instanceof PublicKey) {
+ validator.initVerify((PublicKey) key);
+ validator.update(rawQuery.getBytes("UTF-8"));
+ } else {
+ throw new VerificationException("Invalid key locator for signature verification");
+ }
if (!validator.verify(decodedSignature)) {
throw new VerificationException("Invalid query param signature");
}
@@ -123,5 +167,32 @@ public class SamlProtocolUtils {
}
}
+ private static String getMessageSigningKeyId(SAML2Object doc) {
+ final ExtensionsType extensions;
+ if (doc instanceof RequestAbstractType) {
+ extensions = ((RequestAbstractType) doc).getExtensions();
+ } else if (doc instanceof StatusResponseType) {
+ extensions = ((StatusResponseType) doc).getExtensions();
+ } else {
+ return null;
+ }
+ if (extensions == null) {
+ return null;
+ }
+
+ for (Object ext : extensions.getAny()) {
+ if (! (ext instanceof Element)) {
+ continue;
+ }
+
+ String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
+
+ if (res != null) {
+ return res;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 40f615eaec..b3994c18a7 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -408,14 +408,17 @@ public class SamlService extends AuthorizationEndpointBase {
builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
+ boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
if (samlClient.requiresRealmSignature()) {
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
-
+ if (! postBinding) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
- if (SamlProtocol.SAML_POST_BINDING.equals(logoutBinding)) {
+ if (postBinding) {
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
@@ -477,7 +480,8 @@ public class SamlService extends AuthorizationEndpointBase {
return;
}
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
- SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
+ KeyLocator clientKeyLocator = new HardcodedKeyLocator(publicKey);
+ SamlProtocolUtils.verifyRedirectSignature(documentHolder, clientKeyLocator, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
}
@Override