Prefer cert over pubkey in SAML metadata

If SAML key material was given as a certificate, consistently
expose the certificate rather than just the public key when
presenting SAML metadata info. This change ensures that the
client obtains sufficient information (eg. issuer) to close
the trust chain.

Closes: #17549

Signed-off-by: Daniel Kobras <kobras@puzzle-itc.de>
This commit is contained in:
Daniel Kobras 2023-03-09 10:13:04 +01:00 committed by Marek Posolda
parent 142bb30f66
commit a45b5dcd90
4 changed files with 48 additions and 12 deletions

View file

@ -134,14 +134,11 @@ public class SAML2Signature {
dto.setSignatureMethod(signatureMethod); dto.setSignatureMethod(signatureMethod);
dto.setReferenceURI(referenceURI); dto.setReferenceURI(referenceURI);
dto.setNextSibling(sibling); dto.setNextSibling(sibling);
dto.setX509Certificate(x509Certificate);
if (x509Certificate != null) {
dto.setX509Certificate(x509Certificate);
}
return XMLSignatureUtil.sign(dto, canonicalizationMethodType); return XMLSignatureUtil.sign(dto, canonicalizationMethodType);
} }
return XMLSignatureUtil.sign(doc, keyName, keyPair, digestMethod, signatureMethod, referenceURI, canonicalizationMethodType); return XMLSignatureUtil.sign(doc, keyName, keyPair, digestMethod, signatureMethod, referenceURI, x509Certificate, canonicalizationMethodType);
} }
/** /**

View file

@ -723,9 +723,7 @@ public class XMLSignatureUtil {
if (x509Certificate != null) { if (x509Certificate != null) {
items.add(keyInfoFactory.newX509Data(Collections.singletonList(x509Certificate))); items.add(keyInfoFactory.newX509Data(Collections.singletonList(x509Certificate)));
} } else if (publicKey != null) {
if (publicKey != null) {
items.add(keyInfoFactory.newKeyValue(publicKey)); items.add(keyInfoFactory.newKeyValue(publicKey));
} }

View file

@ -89,6 +89,7 @@ import javax.xml.stream.XMLStreamWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.net.URI; import java.net.URI;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
@ -457,13 +458,15 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
if (getConfig().isSignSpMetadata()) if (getConfig().isSignSpMetadata())
{ {
KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm); KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(activeKey.getKid(), activeKey.getCertificate()); X509Certificate certificate = activeKey.getCertificate();
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(activeKey.getKid(), certificate);
KeyPair keyPair = new KeyPair(activeKey.getPublicKey(), activeKey.getPrivateKey()); KeyPair keyPair = new KeyPair(activeKey.getPublicKey(), activeKey.getPrivateKey());
Document metadataDocument = DocumentUtil.getDocument(descriptor); Document metadataDocument = DocumentUtil.getDocument(descriptor);
SAML2Signature signatureHelper = new SAML2Signature(); SAML2Signature signatureHelper = new SAML2Signature();
signatureHelper.setSignatureMethod(getSignatureAlgorithm().getXmlSignatureMethod()); signatureHelper.setSignatureMethod(getSignatureAlgorithm().getXmlSignatureMethod());
signatureHelper.setDigestMethod(getSignatureAlgorithm().getXmlSignatureDigestMethod()); signatureHelper.setDigestMethod(getSignatureAlgorithm().getXmlSignatureDigestMethod());
signatureHelper.setX509Certificate(certificate);
Node nextSibling = metadataDocument.getDocumentElement().getFirstChild(); Node nextSibling = metadataDocument.getDocumentElement().getFirstChild();
signatureHelper.setNextSibling(nextSibling); signatureHelper.setNextSibling(nextSibling);

View file

@ -30,6 +30,7 @@ import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
@ -41,15 +42,19 @@ import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep; import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.KeyUtils;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
@ -67,7 +72,11 @@ import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -93,6 +102,8 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.XMLDSIG_NSURI; import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.XMLDSIG_NSURI;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -1044,7 +1055,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
} }
@Test @Test
public void testSamlExportSignatureOn() throws URISyntaxException, IOException, ConfigurationException, ParsingException, ProcessingException { public void testSamlExportSignatureOn() throws URISyntaxException, IOException, ConfigurationException, ParsingException, ProcessingException, CertificateEncodingException {
// Use import-config to convert IDPSSODescriptor file into key value pairs // Use import-config to convert IDPSSODescriptor file into key value pairs
// to use when creating a SAML Identity Provider // to use when creating a SAML Identity Provider
MultipartFormDataOutput form = new MultipartFormDataOutput(); MultipartFormDataOutput form = new MultipartFormDataOutput();
@ -1059,6 +1070,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
// Explicitly enable SP Metadata Signature // Explicitly enable SP Metadata Signature
result.put(SAMLIdentityProviderConfig.SIGN_SP_METADATA, "true"); result.put(SAMLIdentityProviderConfig.SIGN_SP_METADATA, "true");
result.put(SAMLIdentityProviderConfig.XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.CERT_SUBJECT.name());
// Create new SAML identity provider using configuration retrieved from import-config // Create new SAML identity provider using configuration retrieved from import-config
IdentityProviderRepresentation idpRep = createRep("saml", "saml", true, result); IdentityProviderRepresentation idpRep = createRep("saml", "saml", true, result);
@ -1073,6 +1085,32 @@ public class IdentityProviderTest extends AbstractAdminTest {
Document document = DocumentUtil.getDocument(body); Document document = DocumentUtil.getDocument(body);
Element signatureElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), XMLDSIG_NSURI.get(), "Signature"); Element signatureElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), XMLDSIG_NSURI.get(), "Signature");
Assert.assertNotNull(signatureElement); Assert.assertThat("Signature not null", signatureElement, notNullValue());
Element keyInfoElement = DocumentUtil.getDirectChildElement(signatureElement, XMLDSIG_NSURI.get(), "KeyInfo");
Assert.assertThat("KeyInfo not null", keyInfoElement, notNullValue());
Element x509DataElement = DocumentUtil.getDirectChildElement(keyInfoElement, XMLDSIG_NSURI.get(), "X509Data");
Assert.assertThat("X509Data not null", x509DataElement, notNullValue());
Element x509CertificateElement = DocumentUtil.getDirectChildElement(x509DataElement, XMLDSIG_NSURI.get(), "X509Certificate");
Assert.assertThat("X509Certificate not null", x509CertificateElement, notNullValue());
Element keyNameElement = DocumentUtil.getDirectChildElement(keyInfoElement, XMLDSIG_NSURI.get(), "KeyName");
Assert.assertThat("KeyName not null", keyNameElement, notNullValue());
String activeSigCert = KeyUtils.findActiveSigningKey(realm, Constants.DEFAULT_SIGNATURE_ALGORITHM).getCertificate();
Assert.assertThat("activeSigCert not null", activeSigCert, notNullValue());
X509Certificate activeX509SigCert = XMLSignatureUtil.getX509CertificateFromKeyInfoString(activeSigCert);
Assert.assertThat("KeyName matches subject DN",
keyNameElement.getTextContent().trim(), equalTo(activeX509SigCert.getSubjectDN().getName()));
Assert.assertThat("Signing cert matches active realm cert",
x509CertificateElement.getTextContent().trim(), equalTo(Base64.getEncoder().encodeToString(activeX509SigCert.getEncoded())));
PublicKey activePublicSigKey = activeX509SigCert.getPublicKey();
Assert.assertThat("Metadata signature is valid",
new SAML2Signature().validate(document, new HardcodedKeyLocator(activePublicSigKey)), is(true));
} }
} }