From dc8b759c3dc2909b8c62ae5e29713e50ce91535a Mon Sep 17 00:00:00 2001 From: laskasn Date: Tue, 13 Dec 2022 12:58:41 +0100 Subject: [PATCH] Use encryption keys rather than sig for crypto in SAML Closes #13606 Co-authored-by: mhajas Co-authored-by: hmlnarik --- .../AbstractSamlAuthenticationHandler.java | 3 +- .../java/org/keycloak/crypto/Algorithm.java | 9 + .../client/resource/ComponentsResource.java | 5 + .../common/constants/JBossSAMLConstants.java | 1 + .../keycloak/saml/SPMetadataDescriptor.java | 36 ++-- .../core/saml/v2/util/AssertionUtil.java | 41 +++-- .../saml/v2/writers/SAMLMetadataWriter.java | 15 ++ .../core/util/XMLEncryptionUtil.java | 80 ++++++--- .../core/parsers/saml/SAMLParserTest.java | 2 +- .../core/saml/v2/util/AssertionUtilTest.java | 4 +- .../models/utils/DefaultKeyProviders.java | 22 ++- .../keycloak/broker/saml/SAMLEndpoint.java | 39 +++- .../broker/saml/SAMLIdentityProvider.java | 63 ++++--- .../GeneratedRsaEncKeyProviderFactory.java | 7 +- .../ImportedRsaEncKeyProviderFactory.java | 7 +- .../saml/SAMLDecryptionKeysLocator.java | 166 ++++++++++++++++++ .../saml/SAMLEncryptionAlgorithms.java | 60 +++++++ .../keycloak/protocol/saml/SamlProtocol.java | 10 +- .../protocol/saml/SamlProtocolUtils.java | 22 --- .../SamlSPDescriptorClientInstallation.java | 21 ++- .../org/keycloak/testsuite/admin/ApiUtil.java | 23 +-- .../org/keycloak/testsuite/util/KeyUtils.java | 66 +++++-- .../OIDCPublicKeyRotationAdapterTest.java | 59 ++++--- .../admin/client/InstallationTest.java | 10 +- .../admin/group/AbstractGroupTest.java | 3 +- .../AbstractKcSamlEncryptedElementsTest.java | 165 +++++++++++++++++ .../broker/KcOIDCBrokerWithSignatureTest.java | 11 +- .../broker/KcOidcBrokerPrivateKeyJwtTest.java | 2 +- .../broker/KcSamlEncryptedAssertionTest.java | 82 +++++++++ .../broker/KcSamlEncryptedIdTest.java | 128 +++++--------- .../broker/KcSamlSignedBrokerTest.java | 70 +++----- .../broker/KcSamlSpDescriptorTest.java | 129 +++++++++++++- .../keys/ImportedRsaKeyProviderTest.java | 2 +- .../migration/AbstractMigrationTest.java | 2 +- .../oauth/ClientTokenExchangeSAML2Test.java | 7 +- .../oidc/OIDCAdvancedRequestParamsTest.java | 8 +- .../keycloak/testsuite/oidc/UserInfoTest.java | 3 +- .../testsuite/saml/ArtifactBindingTest.java | 2 +- 38 files changed, 1035 insertions(+), 350 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/saml/SAMLDecryptionKeysLocator.java create mode 100644 services/src/main/java/org/keycloak/protocol/saml/SAMLEncryptionAlgorithms.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractKcSamlEncryptedElementsTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedAssertionTest.java 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 7b728ade620..d701295611b 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 @@ -533,7 +533,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic // We'll need to decrypt it first. Document encryptedAssertionDocument = DocumentUtil.createDocument(); encryptedAssertionDocument.appendChild(encryptedAssertionDocument.importNode(encryptedAssertion, true)); - return XMLEncryptionUtil.decryptElementInDocument(encryptedAssertionDocument, deployment.getDecryptionKey()); + + return XMLEncryptionUtil.decryptElementInDocument(encryptedAssertionDocument, data -> Collections.singletonList(deployment.getDecryptionKey())); } return DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get())); } diff --git a/core/src/main/java/org/keycloak/crypto/Algorithm.java b/core/src/main/java/org/keycloak/crypto/Algorithm.java index 94d8963bce6..dbba7942d97 100755 --- a/core/src/main/java/org/keycloak/crypto/Algorithm.java +++ b/core/src/main/java/org/keycloak/crypto/Algorithm.java @@ -16,8 +16,11 @@ */ package org.keycloak.crypto; +import org.keycloak.common.crypto.CryptoConstants; + public interface Algorithm { + /* RSA signing algorithms */ String HS256 = "HS256"; String HS384 = "HS384"; String HS512 = "HS512"; @@ -31,5 +34,11 @@ public interface Algorithm { String PS384 = "PS384"; String PS512 = "PS512"; + /* RSA Encryption Algorithms */ + String RSA1_5 = CryptoConstants.RSA1_5; + String RSA_OAEP = CryptoConstants.RSA_OAEP; + String RSA_OAEP_256 = CryptoConstants.RSA_OAEP_256; + + /* AES */ String AES = "AES"; } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ComponentsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ComponentsResource.java index 6f0b9735415..235b34c845d 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ComponentsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ComponentsResource.java @@ -19,6 +19,7 @@ package org.keycloak.admin.client.resource; import org.keycloak.representations.idm.ComponentRepresentation; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -59,5 +60,9 @@ public interface ComponentsResource { @Path("{id}") ComponentResource component(@PathParam("id") String id); + @Path("{id}") + @DELETE + ComponentResource removeComponent(@PathParam("id") String id); + } diff --git a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java index 2615e58b2bb..59f8805cdc6 100755 --- a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java +++ b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java @@ -155,6 +155,7 @@ public enum JBossSAMLConstants { // Attribute names and other constants ADDRESS("Address"), + ALGORITHM("Algorithm"), ALLOW_CREATE("AllowCreate"), ASSERTION_CONSUMER_SERVICE_URL("AssertionConsumerServiceURL"), ASSERTION_CONSUMER_SERVICE_INDEX("AssertionConsumerServiceIndex"), diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java index 4dae9a06ce7..114cd3832dd 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java +++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java @@ -30,6 +30,7 @@ import org.keycloak.dom.saml.v2.metadata.IndexedEndpointType; import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; +import org.keycloak.dom.xmlsec.w3.xmlenc.EncryptionMethodType; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -43,9 +44,9 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_ */ public class SPMetadataDescriptor { - public static EntityDescriptorType buildSPdescriptor(URI loginBinding, URI logoutBinding, URI assertionEndpoint, URI logoutEndpoint, - boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted, - String entityId, String nameIDPolicyFormat, List signingCerts, List encryptionCerts) + public static EntityDescriptorType buildSPDescriptor(URI loginBinding, URI logoutBinding, URI assertionEndpoint, URI logoutEndpoint, + boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted, + String entityId, String nameIDPolicyFormat, List signingCerts, List encryptionCerts) { EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId); entityDescriptor.setID(IDGenerator.create("ID_")); @@ -57,22 +58,14 @@ public class SPMetadataDescriptor { spSSODescriptor.addSingleLogoutService(new EndpointType(logoutBinding, logoutEndpoint)); if (wantAuthnRequestsSigned && signingCerts != null) { - for (Element key: signingCerts) - { - KeyDescriptorType keyDescriptor = new KeyDescriptorType(); - keyDescriptor.setUse(KeyTypes.SIGNING); - keyDescriptor.setKeyInfo(key); - spSSODescriptor.addKeyDescriptor(keyDescriptor); + for (KeyDescriptorType key: signingCerts) { + spSSODescriptor.addKeyDescriptor(key); } } if (wantAssertionsEncrypted && encryptionCerts != null) { - for (Element key: encryptionCerts) - { - KeyDescriptorType keyDescriptor = new KeyDescriptorType(); - keyDescriptor.setUse(KeyTypes.ENCRYPTION); - keyDescriptor.setKeyInfo(key); - spSSODescriptor.addKeyDescriptor(keyDescriptor); + for (KeyDescriptorType key: encryptionCerts) { + spSSODescriptor.addKeyDescriptor(key); } } @@ -86,6 +79,19 @@ public class SPMetadataDescriptor { return entityDescriptor; } + public static KeyDescriptorType buildKeyDescriptorType(Element keyInfo, KeyTypes use, String algorithm) { + KeyDescriptorType keyDescriptor = new KeyDescriptorType(); + keyDescriptor.setUse(use); + keyDescriptor.setKeyInfo(keyInfo); + + if (algorithm != null) { + EncryptionMethodType encMethod = new EncryptionMethodType(algorithm); + keyDescriptor.addEncryptionMethod(encMethod); + } + + return keyDescriptor; + } + public static Element buildKeyInfoElement(String keyName, String pemEncodedCertificate) throws javax.xml.parsers.ParserConfigurationException { 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 cf2685dc751..c4f8f7a0bcf 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 @@ -61,7 +61,6 @@ import org.w3c.dom.Node; import javax.xml.crypto.dsig.XMLSignature; import javax.xml.datatype.XMLGregorianCalendar; -import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import java.io.ByteArrayInputStream; @@ -69,7 +68,9 @@ import java.io.ByteArrayOutputStream; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -566,7 +567,7 @@ public class AssertionUtil { if (privateKey == null) { throw new ProcessingException("Encryptd assertion and decrypt private key is null"); } - decryptAssertion(holder, responseType, privateKey); + decryptAssertion(responseType, privateKey); } return responseType.getAssertions().get(0).getAssertion(); @@ -583,25 +584,32 @@ public class AssertionUtil { return rtChoiceType.getEncryptedAssertion() != null; } + public static Element decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException { + return decryptAssertion(responseType, encryptedData -> Collections.singletonList(privateKey)); + } + /** * This method modifies the given responseType, and replaces the encrypted assertion with a decrypted version. - * @param responseType a response containg an encrypted assertion + * + * @param responseType a response containing an encrypted assertion + * @param decryptionKeyLocator locator of keys suitable for decrypting encrypted element + * * @return the assertion element as it was decrypted. This can be used in signature verification. */ - public static Element decryptAssertion(SAMLDocumentHolder holder, ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException { - Document doc = holder.getSamlDocument(); - Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get())); - - if (enc == null) { - throw new ProcessingException("No encrypted assertion found."); - } + public static Element decryptAssertion(ResponseType responseType, XMLEncryptionUtil.DecryptionKeyLocator decryptionKeyLocator) throws ParsingException, ProcessingException, ConfigurationException { + Element enc = responseType.getAssertions().stream() + .map(ResponseType.RTChoiceType::getEncryptedAssertion) + .filter(Objects::nonNull) + .findFirst() + .map(EncryptedElementType::getEncryptedElement) + .orElseThrow(() -> new ProcessingException("No encrypted assertion found.")); String oldID = enc.getAttribute(JBossSAMLConstants.ID.get()); Document newDoc = DocumentUtil.createDocument(); Node importedNode = newDoc.importNode(enc, true); newDoc.appendChild(importedNode); - Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey); + Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, decryptionKeyLocator); SAMLParser parser = SAMLParser.getInstance(); JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement); @@ -618,7 +626,14 @@ public class AssertionUtil { return subTypeElement != null && subTypeElement.getEncryptedID() != null; } - public static void decryptId(final ResponseType responseType, final PrivateKey privateKey) throws ConfigurationException, ProcessingException, ParsingException { + /** + * This method modifies the given responseType, and replaces the encrypted id with a decrypted version. + * + * @param responseType a response containing an encrypted id + * @param decryptionKeyLocator locator of keys suitable for decrypting encrypted element + * + */ + public static void decryptId(final ResponseType responseType, XMLEncryptionUtil.DecryptionKeyLocator decryptionKeyLocator) throws ConfigurationException, ProcessingException, ParsingException { final STSubType subTypeElement = getSubTypeElement(responseType); if(subTypeElement == null) { return; @@ -631,7 +646,7 @@ public class AssertionUtil { Document newDoc = DocumentUtil.createDocument(); Node importedNode = newDoc.importNode(encryptedElement, true); newDoc.appendChild(importedNode); - Element decryptedNameIdElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey); + Element decryptedNameIdElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, decryptionKeyLocator); final XMLEventReader xmlEventReader = StaxParserUtil.getXMLEventReader(DocumentUtil.getNodeAsStream(decryptedNameIdElement)); NameIDType nameIDType = SAMLParserUtil.parseNameIDType(xmlEventReader); diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLMetadataWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLMetadataWriter.java index 21203e39d45..75a744befaa 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLMetadataWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLMetadataWriter.java @@ -40,6 +40,7 @@ import org.keycloak.dom.saml.v2.metadata.RequestedAttributeType; import org.keycloak.dom.saml.v2.metadata.RoleDescriptorType; import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; import org.keycloak.dom.saml.v2.metadata.SSODescriptorType; +import org.keycloak.dom.xmlsec.w3.xmlenc.EncryptionMethodType; import org.keycloak.saml.common.constants.JBossSAMLConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ProcessingException; @@ -499,10 +500,24 @@ public class SAMLMetadataWriter extends BaseWriter { Element keyInfo = keyDescriptor.getKeyInfo(); StaxUtil.writeDOMElement(writer, keyInfo); + + List encryptionMethodTypes = keyDescriptor.getEncryptionMethod(); + if (encryptionMethodTypes != null && !encryptionMethodTypes.isEmpty()) { + for (EncryptionMethodType encryptionMethodType : encryptionMethodTypes) { + writeEncryptionMethod(encryptionMethodType); + } + } + StaxUtil.writeEndElement(writer); StaxUtil.flush(writer); } + public void writeEncryptionMethod(EncryptionMethodType methodType) throws ProcessingException { + StaxUtil.writeStartElement(writer, METADATA_PREFIX, JBossSAMLConstants.ENCRYPTION_METHOD.get(), JBossSAMLURIConstants.METADATA_NSURI.get()); + StaxUtil.writeAttribute(writer, JBossSAMLConstants.ALGORITHM.get(), methodType.getAlgorithm()); + StaxUtil.writeEndElement(writer); + } + public void writeAttributeService(EndpointType endpoint) throws ProcessingException { StaxUtil.writeStartElement(writer, METADATA_PREFIX, JBossSAMLConstants.ATTRIBUTE_SERVICE.get(), JBossSAMLURIConstants.METADATA_NSURI.get()); diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java index b180e6cbd6a..54d8fff04b9 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java @@ -38,6 +38,7 @@ import javax.xml.namespace.QName; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.List; import java.util.Objects; import javax.xml.XMLConstants; import javax.xml.crypto.dsig.XMLSignature; @@ -52,6 +53,18 @@ import javax.xml.crypto.dsig.XMLSignature; */ public class XMLEncryptionUtil { + public interface DecryptionKeyLocator { + + /** + * Provides a list of private keys that are suitable for decrypting + * the given {@code encryptedData}. + * + * @param encryptedData data that need to be decrypted + * @return a list of private keys + */ + List getKeys(EncryptedData encryptedData); + } + private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); static { @@ -86,13 +99,11 @@ public class XMLEncryptionUtil { * @throws org.keycloak.saml.common.exceptions.ProcessingException */ private static EncryptedKey encryptKey(Document document, SecretKey keyToBeEncrypted, PublicKey keyUsedToEncryptSecretKey, - int keySize) throws ProcessingException { + int keySize, String encryptionUrlForKeyUnwrap) throws ProcessingException { XMLCipher keyCipher; - String pubKeyAlg = keyUsedToEncryptSecretKey.getAlgorithm(); try { - String keyWrapAlgo = getXMLEncryptionURLForKeyUnwrap(pubKeyAlg, keySize); - keyCipher = XMLCipher.getInstance(keyWrapAlgo); + keyCipher = XMLCipher.getInstance(encryptionUrlForKeyUnwrap); keyCipher.init(XMLCipher.WRAP_MODE, keyUsedToEncryptSecretKey); return keyCipher.encryptKey(document, keyToBeEncrypted); @@ -101,6 +112,12 @@ public class XMLEncryptionUtil { } } + public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey, + int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo) throws ProcessingException { + encryptElement(elementQName, document, publicKey, secretKey, keySize, wrappingElementQName, addEncryptedKeyInKeyInfo, + getXMLEncryptionURLForKeyUnwrap(publicKey.getAlgorithm(), keySize)); + } + /** * Given an element in a Document, encrypt the element and replace the element in the document with the encrypted * data @@ -116,7 +133,7 @@ public class XMLEncryptionUtil { * @throws ProcessingException */ public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey, - int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo) throws ProcessingException { + int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo, String encryptionUrlForKeyUnwrap) throws ProcessingException { if (elementQName == null) throw logger.nullArgumentError("elementQName"); if (document == null) @@ -131,7 +148,7 @@ public class XMLEncryptionUtil { throw logger.domMissingDocElementError(elementQName.toString()); XMLCipher cipher = null; - EncryptedKey encryptedKey = encryptKey(document, secretKey, publicKey, keySize); + EncryptedKey encryptedKey = encryptKey(document, secretKey, publicKey, keySize, encryptionUrlForKeyUnwrap); String encryptionAlgorithm = getXMLEncryptionURL(secretKey.getAlgorithm(), keySize); // Encrypt the Document @@ -197,14 +214,18 @@ public class XMLEncryptionUtil { } /** - * Decrypt an encrypted element inside a document + * Decrypts an encrypted element inside a document. It tries to use all + * keys provided by {@code decryptionKeyLocator} and if it does not + * succeed it throws {@link ProcessingException}. * - * @param documentWithEncryptedElement - * @param privateKey key need to unwrap the encryption key + * @param documentWithEncryptedElement document containing encrypted element + * @param decryptionKeyLocator decryption key locator * * @return the document with the encrypted element replaced by the data element + * + * @throws ProcessingException when decrypting was not successful */ - public static Element decryptElementInDocument(Document documentWithEncryptedElement, PrivateKey privateKey) + public static Element decryptElementInDocument(Document documentWithEncryptedElement, DecryptionKeyLocator decryptionKeyLocator) throws ProcessingException { if (documentWithEncryptedElement == null) throw logger.nullArgumentError("Input document is null"); @@ -242,21 +263,38 @@ public class XMLEncryptionUtil { Document decryptedDoc = null; if (encryptedData != null && encryptedKey != null) { - try { - String encAlgoURL = encryptedData.getEncryptionMethod().getAlgorithm(); - XMLCipher keyCipher = XMLCipher.getInstance(); - keyCipher.init(XMLCipher.UNWRAP_MODE, privateKey); - Key encryptionKey = keyCipher.decryptKey(encryptedKey, encAlgoURL); - cipher = XMLCipher.getInstance(); - cipher.init(XMLCipher.DECRYPT_MODE, encryptionKey); + boolean success = false; + final Exception enclosingThrowable = new RuntimeException("Cannot decrypt element in document"); + List encryptionKeys; + encryptionKeys = decryptionKeyLocator.getKeys(encryptedData); - decryptedDoc = cipher.doFinal(documentWithEncryptedElement, encDataElement); - } catch (Exception e) { - throw logger.processingError(e); + if (encryptionKeys == null || encryptionKeys.isEmpty()) { + throw logger.nullValueError("Key for EncryptedData not found."); + } + + for (PrivateKey privateKey : encryptionKeys) { + try { + String encAlgoURL = encryptedData.getEncryptionMethod().getAlgorithm(); + XMLCipher keyCipher = XMLCipher.getInstance(); + keyCipher.init(XMLCipher.UNWRAP_MODE, privateKey); + Key encryptionKey = keyCipher.decryptKey(encryptedKey, encAlgoURL); + cipher = XMLCipher.getInstance(); + cipher.init(XMLCipher.DECRYPT_MODE, encryptionKey); + + decryptedDoc = cipher.doFinal(documentWithEncryptedElement, encDataElement); + success = true; + break; + } catch (Exception e) { + enclosingThrowable.addSuppressed(e); + } + } + + if (!success) { + throw logger.processingError(enclosingThrowable); } } - if(decryptedDoc == null){ + if (decryptedDoc == null) { throw logger.nullValueError("decryptedDoc"); } diff --git a/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java index d361fb8b298..3fa666b1f80 100644 --- a/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java +++ b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java @@ -218,7 +218,7 @@ public class SAMLParserTest { assertNotNull(rtChoiceType.getEncryptedAssertion()); PrivateKey privateKey = DerUtils.decodePrivateKey(Base64.decode(PRIVATE_KEY)); - AssertionUtil.decryptAssertion(holder, resp, privateKey); + AssertionUtil.decryptAssertion(resp, privateKey); rtChoiceType = resp.getAssertions().get(0); assertNotNull(rtChoiceType.getAssertion()); diff --git a/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java b/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java index dea29500fff..ecf5b98f88f 100644 --- a/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java +++ b/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java @@ -13,6 +13,7 @@ import java.io.InputStream; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collections; import java.util.Scanner; import org.junit.BeforeClass; @@ -86,7 +87,8 @@ public class AssertionUtilTest { assertNotNull(subType.getEncryptedID()); assertNull(subType.getBaseID()); - AssertionUtil.decryptId(responseType, extractPrivateKey()); + PrivateKey pk = extractPrivateKey(); + AssertionUtil.decryptId(responseType, data -> Collections.singletonList(pk)); assertNull(subType.getEncryptedID()); assertNotNull(subType.getBaseID()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java index 828f4254e72..6c264c4978e 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java @@ -72,7 +72,7 @@ public class DefaultKeyProviders { MultivaluedHashMap config = new MultivaluedHashMap<>(); config.putSingle("priority", DEFAULT_PRIORITY); config.putSingle("keyUse", KeyUse.ENC.name()); - config.putSingle("algorithm", JWEConstants.RSA_OAEP); + config.putSingle("algorithm", Algorithm.RSA_OAEP); generated.setConfig(config); realm.addComponentModel(generated); @@ -123,6 +123,7 @@ public class DefaultKeyProviders { rsa.setProviderType(KeyProvider.class.getName()); MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("keyUse", KeyUse.SIG.getSpecName()); config.putSingle("priority", DEFAULT_PRIORITY); config.putSingle("privateKey", privateKeyPem); if (certificatePem != null) { @@ -133,6 +134,25 @@ public class DefaultKeyProviders { realm.addComponentModel(rsa); } + if (!hasProvider(realm, "rsa-enc")) { + ComponentModel rsaEnc = new ComponentModel(); + rsaEnc.setName("rsa-enc"); + rsaEnc.setParentId(realm.getId()); + rsaEnc.setProviderId("rsa-enc"); + rsaEnc.setProviderType(KeyProvider.class.getName()); + + MultivaluedHashMap configEnc = new MultivaluedHashMap<>(); + configEnc.putSingle("keyUse", KeyUse.ENC.getSpecName()); + configEnc.putSingle("priority", "100"); + configEnc.putSingle("privateKey", privateKeyPem); + if (certificatePem != null) { + configEnc.putSingle("certificate", certificatePem); + } + rsaEnc.setConfig(configEnc); + + realm.addComponentModel(rsaEnc); + } + createSecretProvider(realm); createAesProvider(realm); } 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 042763ac69a..91c3b72ea21 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -54,6 +54,7 @@ import org.keycloak.protocol.saml.SamlProtocolUtils; import org.keycloak.protocol.saml.SamlService; import org.keycloak.protocol.saml.SamlSessionUtils; import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor; +import org.keycloak.protocol.saml.SAMLDecryptionKeysLocator; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.common.constants.GeneralConstants; @@ -148,6 +149,9 @@ public class SAMLEndpoint { private final HttpHeaders headers; + public static final String ENCRYPTION_DEPRECATED_MODE_PROPERTY = "keycloak.saml.deprecated.encryption"; + private final boolean DEPRECATED_ENCRYPTION = Boolean.getBoolean(ENCRYPTION_DEPRECATED_MODE_PROPERTY); + public SAMLEndpoint(KeycloakSession session, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback, DestinationValidator destinationValidator) { this.realm = session.getContext().getRealm(); @@ -415,7 +419,6 @@ public class SAMLEndpoint { } session.getContext().setAuthenticationSession(authSession); - KeyManager.ActiveRsaKey keys = SamlProtocolUtils.getDecryptionKey(session, realm, config); if (! isSuccessfulSamlResponse(responseType)) { String statusMessage = responseType.getStatus() == null || responseType.getStatus().getStatusMessage() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage(); return callback.error(statusMessage); @@ -433,11 +436,22 @@ public class SAMLEndpoint { return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER); } - Element assertionElement; + Element assertionElement = null; if (assertionIsEncrypted) { - // This methods writes the parsed and decrypted assertion back on the responseType parameter: - assertionElement = AssertionUtil.decryptAssertion(holder, responseType, keys.getPrivateKey()); + try { + /* This code is deprecated and will be removed in Keycloak 24 */ + if (DEPRECATED_ENCRYPTION) { + KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm); + assertionElement = AssertionUtil.decryptAssertion(responseType, keys.getPrivateKey()); + } else { + /* End of deprecated code */ + assertionElement = AssertionUtil.decryptAssertion(responseType, new SAMLDecryptionKeysLocator(session, realm, config.getEncryptionAlgorithm())); + } + } catch (ProcessingException ex) { + logger.warnf(ex, "Not possible to decrypt SAML assertion. Please check realm keys of usage ENC in the realm '%s' and make sure there is a key able to decrypt the assertion encrypted by identity provider '%s'", realm.getName(), config.getAlias()); + throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); + } } else { /* We verify the assertion using original document to handle cases where the IdP includes whitespace and/or newlines inside tags. */ @@ -477,9 +491,20 @@ public class SAMLEndpoint { return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER); } - if(AssertionUtil.isIdEncrypted(responseType)) { - // This methods writes the parsed and decrypted id back on the responseType parameter: - AssertionUtil.decryptId(responseType, keys.getPrivateKey()); + if (AssertionUtil.isIdEncrypted(responseType)) { + try { + /* This code is deprecated and will be removed in Keycloak 24 */ + if (DEPRECATED_ENCRYPTION) { + KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm); + AssertionUtil.decryptId(responseType, data -> Collections.singletonList(keys.getPrivateKey())); + } else { + /* End of deprecated code */ + AssertionUtil.decryptId(responseType, new SAMLDecryptionKeysLocator(session, realm, config.getEncryptionAlgorithm())); + } + } catch (ProcessingException ex) { + logger.warnf(ex, "Not possible to decrypt SAML encryptedId. Please check realm keys of usage ENC in the realm '%s' and make sure there is a key able to decrypt the encryptedId encrypted by identity provider '%s'", realm.getName(), config.getAlias()); + throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); + } } AssertionType assertion = responseType.getAssertions().get(0).getAssertion(); 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 64a830cdcbc..07f2c727da7 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -26,15 +26,15 @@ import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.KeyStatus; import org.keycloak.crypto.KeyUse; -import org.keycloak.crypto.KeyWrapper; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.metadata.AttributeConsumingServiceType; import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.dom.saml.v2.metadata.LocalizedNameType; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; @@ -53,6 +53,7 @@ import org.keycloak.protocol.saml.SamlService; import org.keycloak.protocol.saml.SamlSessionUtils; import org.keycloak.protocol.saml.mappers.SamlMetadataDescriptorUpdater; import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor; +import org.keycloak.protocol.saml.SAMLEncryptionAlgorithms; import org.keycloak.saml.SAML2AuthnRequestBuilder; import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2NameIDPolicyBuilder; @@ -96,7 +97,6 @@ import java.util.List; import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * @author Pedro Igor @@ -363,15 +363,41 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider signingKeys = streamForExport(session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256), false) + // We export all keys for algorithm RS256, both active and passive so IDP is able to verify signature even + // if a key rotation happens in the meantime + List signingKeys = session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256) + .filter(key -> key.getCertificate() != null) + .sorted(SamlService::compareKeys) + .map(key -> { + try { + return SPMetadataDescriptor.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())); + } catch (ParserConfigurationException e) { + logger.warn("Failed to export SAML SP Metadata!", e); + throw new RuntimeException(e); + } + }) + .map(key -> SPMetadataDescriptor.buildKeyDescriptorType(key, KeyTypes.SIGNING, null)) .collect(Collectors.toList()); - // See also SamlProtocolUtils.getDecryptionKey + // We export only active ENC keys so IDP uses different key as soon as possible if a key rotation happens String encAlg = getConfig().getEncryptionAlgorithm(); - Stream encryptionKeyWrappers = (encAlg != null && !encAlg.trim().isEmpty()) - ? session.keys().getKeysStream(realm, KeyUse.ENC, encAlg) - : session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256); - List encryptionKeys = streamForExport(encryptionKeyWrappers, true) + List encryptionKeys = session.keys().getKeysStream(realm) + .filter(key -> key.getStatus().isActive() && KeyUse.ENC.equals(key.getUse()) + && (encAlg == null || Objects.equals(encAlg, key.getAlgorithmOrDefault())) + && SAMLEncryptionAlgorithms.forKeycloakIdentifier(key.getAlgorithm()) != null + && key.getCertificate() != null) + .sorted(SamlService::compareKeys) + .map(key -> { + Element keyInfo; + try { + keyInfo = SPMetadataDescriptor.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())); + } catch (ParserConfigurationException e) { + logger.warn("Failed to export SAML SP Metadata!", e); + throw new RuntimeException(e); + } + + return SPMetadataDescriptor.buildKeyDescriptorType(keyInfo, KeyTypes.ENCRYPTION, SAMLEncryptionAlgorithms.forKeycloakIdentifier(key.getAlgorithm()).getXmlEncIdentifier()); + }) .collect(Collectors.toList()); // Prepare the metadata descriptor model @@ -379,7 +405,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider streamForExport(Stream keys, boolean checkActive) { - return keys.filter(Objects::nonNull) - .filter(key -> key.getCertificate() != null) - .filter(key -> !checkActive || key.getStatus() == KeyStatus.ACTIVE) - .sorted(SamlService::compareKeys) - .map(key -> { - try { - Element element = SPMetadataDescriptor - .buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())); - return element; - } catch (ParserConfigurationException e) { - logger.warn("Failed to export SAML SP Metadata!", e); - throw new RuntimeException(e); - } - }); - } - public SignatureAlgorithm getSignatureAlgorithm() { String alg = getConfig().getSignatureAlgorithm(); if (alg != null) { diff --git a/services/src/main/java/org/keycloak/keys/GeneratedRsaEncKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedRsaEncKeyProviderFactory.java index a0e9ecc6548..fea6aa601d9 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedRsaEncKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedRsaEncKeyProviderFactory.java @@ -21,6 +21,7 @@ import java.util.List; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyUse; import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.models.KeycloakSession; @@ -68,9 +69,9 @@ public class GeneratedRsaEncKeyProviderFactory extends AbstractGeneratedRsaKeyPr @Override protected boolean isSupportedRsaAlgorithm(String algorithm) { - return algorithm.equals(JWEConstants.RSA1_5) - || algorithm.equals(JWEConstants.RSA_OAEP) - || algorithm.equals(JWEConstants.RSA_OAEP_256); + return algorithm.equals(Algorithm.RSA1_5) + || algorithm.equals(Algorithm.RSA_OAEP) + || algorithm.equals(Algorithm.RSA_OAEP_256); } @Override diff --git a/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java index 7433286f99c..f3b5265a6d6 100644 --- a/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java @@ -18,6 +18,7 @@ package org.keycloak.keys; import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyUse; import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.models.KeycloakSession; @@ -61,9 +62,9 @@ public class ImportedRsaEncKeyProviderFactory extends AbstractImportedRsaKeyProv @Override protected boolean isSupportedRsaAlgorithm(String algorithm) { - return algorithm.equals(JWEConstants.RSA1_5) - || algorithm.equals(JWEConstants.RSA_OAEP) - || algorithm.equals(JWEConstants.RSA_OAEP_256); + return algorithm.equals(Algorithm.RSA1_5) + || algorithm.equals(Algorithm.RSA_OAEP) + || algorithm.equals(Algorithm.RSA_OAEP_256); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/saml/SAMLDecryptionKeysLocator.java b/services/src/main/java/org/keycloak/protocol/saml/SAMLDecryptionKeysLocator.java new file mode 100644 index 00000000000..f1fc2b6e20c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/saml/SAMLDecryptionKeysLocator.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 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.protocol.saml; + + + +import org.apache.xml.security.encryption.EncryptedData; +import org.apache.xml.security.encryption.EncryptedKey; +import org.apache.xml.security.encryption.EncryptionMethod; +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.keys.KeyInfo; +import org.apache.xml.security.keys.content.KeyName; +import org.keycloak.common.util.DerUtils; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.saml.processing.core.util.XMLEncryptionUtil; + +import java.security.Key; +import java.security.PrivateKey; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This implementation locates the decryption keys within realm keys. + * It filters realm keys based on algorithm provided within {@link EncryptedData} + * + * Example of encrypted data: + *
+ * {@code
+ * 
+ *     
+ *     
+ *         
+ *             
+ *             
+ *                 
+ *                     .....
+ *                 
+ *             
+ *         
+ *     
+ *     
+ *         
+ *             ...
+ *         
+ *     
+ * 
+ * }
+ * 
+ * + */ +public class SAMLDecryptionKeysLocator implements XMLEncryptionUtil.DecryptionKeyLocator { + + private final KeycloakSession session; + private final RealmModel realm; + private final String requestedAlgorithm; + + public SAMLDecryptionKeysLocator(KeycloakSession session, RealmModel realm, String requestedAlgorithm) { + this.session = session; + this.realm = realm; + this.requestedAlgorithm = requestedAlgorithm; + } + + private List getKeyNames(KeyInfo keyInfo) { + List keyNames = new LinkedList<>(); + + try { + for (int i = 0; i < keyInfo.lengthKeyName(); i++) { + KeyName keyName = keyInfo.itemKeyName(i); + if (keyName != null) { + keyNames.add(keyName.getKeyName()); + } + } + } catch (XMLSecurityException e) { + throw new IllegalStateException("Cannot load keyNames from document", e); + } + + return keyNames; + } + + private Predicate hasMatchingAlgorithm(String algorithm) { + SAMLEncryptionAlgorithms usedAlgorithm = SAMLEncryptionAlgorithms.forXMLEncIdentifier(algorithm); + + if (usedAlgorithm == null) { + throw new IllegalStateException("Keycloak does not support encryption keys for given algorithm: " + algorithm); + } + + return keyWrapper -> Objects.equals(keyWrapper.getAlgorithmOrDefault(), usedAlgorithm.getKeycloakIdentifier()); + } + + @Override + public List getKeys(EncryptedData encryptedData) { + // Check encryptedData contains keyinfo + KeyInfo keyInfo = encryptedData.getKeyInfo(); + if (keyInfo == null) { + throw new IllegalStateException("EncryptedData does not contain KeyInfo"); + } + + Stream keysStream = session.keys().getKeysStream(realm) + .filter(key -> key.getStatus().isEnabled() && KeyUse.ENC.equals(key.getUse())); + + if (requestedAlgorithm != null && !requestedAlgorithm.trim().isEmpty()) { + keysStream = keysStream.filter(keyWrapper -> Objects.equals(keyWrapper.getAlgorithmOrDefault(), requestedAlgorithm)); + } + + // If encryptedData contains keyName we will use only for keys with given kid + if (keyInfo.containsKeyName()) { + List keyNames = getKeyNames(keyInfo); + keysStream = keysStream.filter(keyWrapper -> keyNames.contains(keyWrapper.getKid())); + } + + // Look for algorithm used inside encryptedData and allow only keys generated for specific algorithm + try { + EncryptedKey encryptedKey = keyInfo.itemEncryptedKey(0); + if (encryptedKey != null) { + EncryptionMethod encryptionMethod = encryptedKey.getEncryptionMethod(); + + if (encryptionMethod == null) { + throw new IllegalArgumentException("KeyInfo does not contain encryption method"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + if (algorithm == null) { + throw new IllegalArgumentException("Not able to find algorithm for given encryption method"); + } + keysStream = keysStream.filter(hasMatchingAlgorithm(algorithm)); + } + } catch (XMLSecurityException e) { + throw new IllegalArgumentException("EncryptedData does not contain KeyInfo ", e); + } + + // Map keys to PrivateKey + return keysStream + .map(KeyWrapper::getPrivateKey) + .map(Key::getEncoded) + .map(encoded -> { + try { + return DerUtils.decodePrivateKey(encoded); + } catch (Exception e) { + throw new RuntimeException("Could not decode private key.", e); + } + }) + .collect(Collectors.toList()); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/saml/SAMLEncryptionAlgorithms.java b/services/src/main/java/org/keycloak/protocol/saml/SAMLEncryptionAlgorithms.java new file mode 100644 index 00000000000..3f942fe6799 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/saml/SAMLEncryptionAlgorithms.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 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.protocol.saml; + +import org.apache.xml.security.encryption.XMLCipher; +import org.keycloak.crypto.Algorithm; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * This enum provides mapping between Keycloak provided encryption algorithms and algorithms from xmlsec. + * It is used to make sure we are using keys generated for given algorithm only with that algorithm. + */ +public enum SAMLEncryptionAlgorithms { + RSA_OAEP(XMLCipher.RSA_OAEP, Algorithm.RSA_OAEP), + RSA1_5(XMLCipher.RSA_v1dot5, Algorithm.RSA1_5); + + private String xmlEncIdentifier; + private String keycloakIdentifier; + private static final Map forXMLEncIdentifier = Arrays.stream(values()).collect(Collectors.toMap(SAMLEncryptionAlgorithms::getXmlEncIdentifier, Function.identity())); + private static final Map forKeycloakIdentifier = Arrays.stream(values()).collect(Collectors.toMap(SAMLEncryptionAlgorithms::getKeycloakIdentifier, Function.identity())); + + SAMLEncryptionAlgorithms(String xmlEncIdentifier, String keycloakIdentifier) { + this.xmlEncIdentifier = xmlEncIdentifier; + this.keycloakIdentifier = keycloakIdentifier; + } + + public String getXmlEncIdentifier() { + return xmlEncIdentifier; + } + public String getKeycloakIdentifier() { + return keycloakIdentifier; + } + + public static SAMLEncryptionAlgorithms forXMLEncIdentifier(String xmlEncIdentifier) { + return forXMLEncIdentifier.get(xmlEncIdentifier); + } + + public static SAMLEncryptionAlgorithms forKeycloakIdentifier(String keycloakIdentifier) { + return forKeycloakIdentifier.get(keycloakIdentifier); + } +} 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 7797416d08d..a5b634fa2f9 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -25,6 +25,9 @@ import org.jboss.logging.Logger; import org.keycloak.broker.saml.SAMLDataMarshaller; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; @@ -85,6 +88,7 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.net.URI; +import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.HashMap; @@ -466,9 +470,9 @@ public class SamlProtocol implements LoginProtocol { Document samlDocument = null; ResponseType samlModel = null; KeyManager keyManager = session.keys(); - KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm); + KeyWrapper keyPair = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); boolean postBinding = isPostBinding(authSession); - String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate()); + String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keyPair.getKid(), keyPair.getCertificate()); String nameId = getSAMLNameId(samlNameIdMappers, nameIdFormat, session, userSession, clientSession); if (nameId == null) { @@ -522,7 +526,7 @@ public class SamlProtocol implements LoginProtocol { if (canonicalization != null) { bindingBuilder.canonicalizationMethod(canonicalization); } - bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()); + bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, (PrivateKey) keyPair.getPrivateKey(), (PublicKey) keyPair.getPublicKey(), keyPair.getCertificate()); if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument(); if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions(); 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 b251830122a..d7178b3ffc5 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -23,19 +23,13 @@ import java.net.URI; import java.security.Key; import org.jboss.logging.Logger; -import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.common.VerificationException; import org.keycloak.common.util.PemUtils; -import org.keycloak.crypto.KeyUse; -import org.keycloak.crypto.KeyWrapper; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType; import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.models.ClientModel; -import org.keycloak.models.KeyManager; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; @@ -132,22 +126,6 @@ public class SamlProtocolUtils { return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE); } - /** - * Returns private key used to decrypt SAML assertions encrypted by 3rd party SAML IDP - */ - public static KeyManager.ActiveRsaKey getDecryptionKey(KeycloakSession session, RealmModel realm, SAMLIdentityProviderConfig idpConfig) { - String encryptionAlgorithm = idpConfig.getEncryptionAlgorithm(); - if (encryptionAlgorithm != null && !encryptionAlgorithm.trim().isEmpty()) { - KeyWrapper kw = session.keys().getActiveKey(realm, KeyUse.ENC, encryptionAlgorithm); - return new KeyManager.ActiveRsaKey(kw); - } else { - // Backwards compatibility. Fallback to return default realm key (which is signature key, even if we're not signing anything, but decrypting stuff) - logger.debugf("Fallback to use default realm RSA key as a key for decrypt SAML documents. It is recommended to configure 'Encryption algorithm' on SAML IDP '%s' and configure encryption key of this algorithm in realm '%s'", - idpConfig.getAlias(), realm.getName()); - return session.keys().getActiveRsaKey(realm); - } - } - public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException { String certPem = client.getAttribute(attribute); return getPublicKey(certPem); diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java index 430f3af0185..653488a2c55 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java @@ -20,6 +20,8 @@ package org.keycloak.protocol.saml.installation; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -32,11 +34,9 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.util.StaxUtil; import org.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter; -import org.w3c.dom.Element; - import java.io.StringWriter; import java.net.URI; -import java.util.Arrays; +import java.util.Collections; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.xml.stream.XMLStreamWriter; @@ -92,17 +92,24 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro String nameIdFormat = samlClient.getNameIDFormat(); if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT; - Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate()); - Element encCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate()); + KeyDescriptorType spCertificate = SPMetadataDescriptor.buildKeyDescriptorType( + SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate()), + KeyTypes.SIGNING, + null); + + KeyDescriptorType encCertificate = SPMetadataDescriptor.buildKeyDescriptorType( + SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate()), + KeyTypes.ENCRYPTION, + null); StringWriter sw = new StringWriter(); XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw); SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer); - EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPdescriptor( + EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPDescriptor( loginBinding, logoutBinding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(), - client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate)); + client.getClientId(), nameIdFormat, Collections.singletonList(spCertificate), Collections.singletonList(encCertificate)); metadataWriter.writeEntityDescriptor(entityDescriptor); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java index 91815030ad4..246d77dc114 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java @@ -23,13 +23,10 @@ import org.keycloak.admin.client.resource.ClientScopeResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.crypto.KeyStatus; -import org.keycloak.crypto.KeyUse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.GroupRepresentation; -import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -43,6 +40,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import static org.junit.Assert.assertEquals; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; /** @@ -269,23 +267,4 @@ public class ApiUtil { return null; } - public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm) { - KeysMetadataRepresentation keyMetadata = realm.keys().getKeyMetadata(); - for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) { - if (rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse())) { - return rep; - } - } - return null; - } - - public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm, String alg) { - KeysMetadataRepresentation keyMetadata = realm.keys().getKeyMetadata(); - for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) { - if (rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse()) && alg.equals(rep.getAlgorithm())) { - return rep; - } - } - return null; - } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java index 20c113b7c52..7763bccff89 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java @@ -1,11 +1,17 @@ package org.keycloak.testsuite.util; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.crypto.KeyStatus; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; +import org.keycloak.keys.KeyProvider; +import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.KeysMetadataRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import javax.ws.rs.core.Response; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; @@ -15,8 +21,10 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; -import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; /** @@ -44,16 +52,7 @@ public class KeyUtils { } } - public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveSigningKey(KeysMetadataRepresentation keys, String algorithm) { - for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) { - if (k.getAlgorithm().equals(algorithm) && KeyStatus.valueOf(k.getStatus()).isActive() && KeyUse.SIG.equals(k.getUse())) { - return k; - } - } - throw new RuntimeException("Active key not found"); - } - - public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveEncKey(KeysMetadataRepresentation keys, String algorithm) { + public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveEncryptionKey(KeysMetadataRepresentation keys, String algorithm) { for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) { if (k.getAlgorithm().equals(algorithm) && KeyStatus.valueOf(k.getStatus()).isActive() && KeyUse.ENC.equals(k.getUse())) { return k; @@ -62,6 +61,51 @@ public class KeyUtils { throw new RuntimeException("Active key not found"); } + public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm) { + return findRealmKeys(realm, rep -> rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse())) + .findFirst() + .orElse(null); + } + + public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm, String alg) { + return findRealmKeys(realm, rep -> rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse()) && alg.equals(rep.getAlgorithm())) + .findFirst() + .orElse(null); + } + + public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveEncryptingKey(RealmResource realm, String alg) { + return findRealmKeys(realm, rep -> rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.ENC.equals(rep.getUse()) && alg.equals(rep.getAlgorithm())) + .findFirst() + .orElse(null); + } + + public static Stream findRealmKeys(RealmResource realm, Predicate filter) { + return realm.keys().getKeyMetadata().getKeys().stream().filter(filter); + } + + public static AutoCloseable generateNewRealmKey(RealmResource realm, KeyUse keyUse, String algorithm, String priority) { + String realmId = realm.toRepresentation().getId(); + + ComponentRepresentation keys = new ComponentRepresentation(); + keys.setName("generated"); + keys.setProviderType(KeyProvider.class.getName()); + keys.setProviderId(keyUse == KeyUse.ENC ? "rsa-enc-generated" : "rsa-generated"); + keys.setParentId(realmId); + keys.setConfig(new MultivaluedHashMap<>()); + keys.getConfig().putSingle("priority", priority); + keys.getConfig().putSingle("keyUse", KeyUse.ENC.getSpecName()); + keys.getConfig().putSingle("algorithm", algorithm); + Response response = realm.components().add(keys); + assertEquals(201, response.getStatus()); + String id = ApiUtil.getCreatedId(response); + response.close(); + + return () -> realm.components().removeComponent(id); + } + + public static AutoCloseable generateNewRealmKey(RealmResource realm, KeyUse keyUse, String algorithm) { + return generateNewRealmKey(realm, keyUse, algorithm, "100"); + } /** * @return key sizes, which are expected to be supported by Keycloak server for {@link org.keycloak.keys.GeneratedRsaKeyProviderFactory} and {@link org.keycloak.keys.GeneratedRsaEncKeyProviderFactory}. */ diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java index c33720dac7c..5a69a8a6a76 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java @@ -36,16 +36,15 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.Time; import org.keycloak.constants.AdapterConstants; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; import org.keycloak.keys.KeyProvider; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; -import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; @@ -59,6 +58,7 @@ import org.keycloak.testsuite.adapter.page.SecurePortal; import org.keycloak.testsuite.adapter.page.TokenMinTTLPage; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.util.KeyUtils; import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.util.URLAssert; import org.openqa.selenium.By; @@ -126,8 +126,9 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes // Logout ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout(); - // Generate new realm key - generateNewRealmKey(); + // Generate new realm keys + generateNewRealmKey(KeyUse.SIG); + generateNewRealmKey(KeyUse.ENC); // Try to login again. It should fail now because not yet allowed to download new keys tokenMinTTLPage.navigateTo(); @@ -189,6 +190,9 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes // KEYCLOAK-3824: Test for public-key-cache-ttl @Test public void testPublicKeyCacheTtl() { + String customerDBUnsecuredUrl = customerDb.getUriBuilder().clone().path("unsecured").path("foo").build().toASCIIString(); + String tokenMinTTLUnsecuredUrl = tokenMinTTLPage.getUriBuilder().clone().path("unsecured").path("foo").build().toASCIIString(); + // increase accessTokenLifespan to 1200 RealmRepresentation demoRealm = adminClient.realm(DEMO).toRepresentation(); demoRealm.setAccessTokenLifespan(1200); @@ -202,9 +206,12 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes int status = invokeRESTEndpoint(accessTokenString); Assert.assertEquals(200, status); - // Re-generate realm public key and remove the old key - String oldActiveKeyProviderId = getActiveKeyProvider(); - generateNewRealmKey(); + // Re-generate realm public key and remove the old key (for both sig and enc) + String oldActiveKeyProviderId = getActiveKeyProviderId(KeyUse.SIG); + generateNewRealmKey(KeyUse.SIG); + adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove(); + oldActiveKeyProviderId = getActiveKeyProviderId(KeyUse.ENC); + generateNewRealmKey(KeyUse.ENC); adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove(); // Send REST request to the customer-db app. Should be still succcessfully authenticated as the JWKPublicKeyLocator cache is still valid @@ -212,15 +219,15 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes Assert.assertEquals(200, status); // TimeOffset to 900 on the REST app side. Token is still valid (1200) but JWKPublicKeyLocator should try to download new key (public-key-cache-ttl=600) - setAdapterAndServerTimeOffset(900, customerDb.toString() + "/unsecured/foo"); + setAdapterAndServerTimeOffset(900, customerDBUnsecuredUrl, tokenMinTTLUnsecuredUrl); // Send REST request. New request to the publicKey cache should be sent, and key is no longer returned as token contains the old kid status = invokeRESTEndpoint(accessTokenString); Assert.assertEquals(401, status); // Revert public keys change and time offset - resetKeycloakDeploymentForAdapter(customerDb.toString() + "/unsecured/foo"); - resetKeycloakDeploymentForAdapter(tokenMinTTLPage.toString() + "/unsecured/foo"); + resetKeycloakDeploymentForAdapter(customerDBUnsecuredUrl); + resetKeycloakDeploymentForAdapter(tokenMinTTLUnsecuredUrl); } @@ -243,16 +250,18 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes String accessTokenString = tokenMinTTLPage.getAccessTokenString(); // Generate new realm public key - String oldActiveKeyProviderId = getActiveKeyProvider(); - - generateNewRealmKey(); + String oldActiveSigKeyProviderId = getActiveKeyProviderId(KeyUse.SIG); + generateNewRealmKey(KeyUse.SIG); + String oldActiveEncKeyProviderId = getActiveKeyProviderId(KeyUse.ENC); + generateNewRealmKey(KeyUse.ENC); // Send REST request to customer-db app. It should be successfully authenticated even that token is signed by the old key int status = invokeRESTEndpoint(accessTokenString); Assert.assertEquals(200, status); - // Remove the old realm key now - adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove(); + // Remove the old realm keys now + adminClient.realm(DEMO).components().component(oldActiveSigKeyProviderId).remove(); + adminClient.realm(DEMO).components().component(oldActiveEncKeyProviderId).remove(); // Set some offset to ensure pushing notBefore will pass setAdapterAndServerTimeOffset(130, customerDBUnsecuredUrl, tokenMinTTLUnsecuredUrl); @@ -287,30 +296,28 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes } - private void generateNewRealmKey() { + private void generateNewRealmKey(KeyUse keyUse) { String realmId = adminClient.realm(DEMO).toRepresentation().getId(); ComponentRepresentation keys = new ComponentRepresentation(); keys.setName("generated"); keys.setProviderType(KeyProvider.class.getName()); - keys.setProviderId("rsa-generated"); + keys.setProviderId(keyUse == KeyUse.SIG ? "rsa-generated" : "rsa-enc-generated"); keys.setParentId(realmId); keys.setConfig(new MultivaluedHashMap<>()); keys.getConfig().putSingle("priority", "150"); + keys.getConfig().putSingle("keyUse", keyUse.getSpecName()); Response response = adminClient.realm(DEMO).components().add(keys); assertEquals(201, response.getStatus()); response.close(); } - private String getActiveKeyProvider() { - KeysMetadataRepresentation keyMetadata = adminClient.realm(DEMO).keys().getKeyMetadata(); - String activeKid = keyMetadata.getActive().get(Algorithm.RS256); - for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) { - if (rep.getKid().equals(activeKid)) { - return rep.getProviderId(); - } - } - return null; + private String getActiveKeyProviderId(KeyUse keyUse) { + KeysMetadataRepresentation.KeyMetadataRepresentation key = keyUse == KeyUse.ENC + ? KeyUtils.findActiveEncryptingKey(adminClient.realm(DEMO), Algorithm.RSA_OAEP) + : KeyUtils.findActiveSigningKey(adminClient.realm(DEMO), Algorithm.RS256); + + return key != null ? key.getProviderId() : null; } private int invokeRESTEndpoint(String accessTokenString) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java index 621b688f7d4..b22f044ad34 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java @@ -35,9 +35,9 @@ import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.testsuite.ProfileAssume; -import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.util.AdminEventPaths; +import org.keycloak.testsuite.util.KeyUtils; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -168,7 +168,7 @@ public class InstallationTest extends AbstractClientTest { private void assertOidcInstallationConfig(String config) { assertThat(config, containsString("test")); - assertThat(config, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getPublicKey()))); + assertThat(config, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getPublicKey()))); assertThat(config, containsString(authServerUrl())); } @@ -182,7 +182,7 @@ public class InstallationTest extends AbstractClientTest { String xml = samlClient.getInstallationProvider("keycloak-saml"); assertThat(xml, containsString("")); assertThat(xml, containsString("SPECIFY YOUR entityID!")); - assertThat(xml, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getCertificate()))); + assertThat(xml, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getCertificate()))); assertThat(xml, containsString(samlUrl())); } @@ -191,7 +191,7 @@ public class InstallationTest extends AbstractClientTest { String cli = samlClient.getInstallationProvider("keycloak-saml-subsystem-cli"); assertThat(cli, containsString("/subsystem=keycloak-saml/secure-deployment=YOUR-WAR.war/")); assertThat(cli, containsString("SPECIFY YOUR entityID!")); - assertThat(cli, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getCertificate()))); + assertThat(cli, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getCertificate()))); assertThat(cli, containsString(samlUrl())); } @@ -209,7 +209,7 @@ public class InstallationTest extends AbstractClientTest { String xml = samlClient.getInstallationProvider("keycloak-saml-subsystem"); assertThat(xml, containsString(" { + System.setProperty(ENCRYPTION_DEPRECATED_MODE_PROPERTY, "true"); + }); + KeysMetadataRepresentation.KeyMetadataRepresentation activeSignatureKey = KeyUtils.findActiveSigningKey(adminClient.realm(bc.consumerRealmName())); + assertThat(activeSignatureKey.getProviderId(), equalTo(sigProviderId)); + sendDocumentWithEncryptedElement(PemUtils.decodePublicKey(activeSignatureKey.getPublicKey()), SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier(), true); + } finally { + // Clear flag + testingClient.server().run(session -> { + System.clearProperty(ENCRYPTION_DEPRECATED_MODE_PROPERTY); + }); + } + } + + @Test + public void testUseDifferentEncryptionAlgorithm() throws Exception { + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + try (AutoCloseable ac = KeyUtils.generateNewRealmKey(realm, KeyUse.ENC, Algorithm.RSA1_5)) { + KeysMetadataRepresentation.KeyMetadataRepresentation key = KeyUtils.findRealmKeys(realm, k -> k.getAlgorithm().equals(Algorithm.RSA1_5)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Cannot find key created on the previous line")); + + sendDocumentWithEncryptedElement(PemUtils.decodePublicKey(key.getPublicKey()), SAMLEncryptionAlgorithms.RSA1_5.getXmlEncIdentifier(), true); + } + } + + protected abstract SamlDocumentStepBuilder.Saml2DocumentTransformer encryptDocument(PublicKey publicKey, String keyEncryptionAlgorithm); + + private void sendDocumentWithEncryptedElement(PublicKey publicKey, String keyEncryptionAlgorithm, boolean shouldPass) throws ConfigurationException, ParsingException, ProcessingException { + createRolesForRealm(bc.consumerRealmName()); + + AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null); + + Document doc = SAML2Request.convert(loginRep); + + final AtomicReference username = new AtomicReference<>(); + assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(0)); + + SamlClientBuilder samlClientBuilder = new SamlClientBuilder() + .authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, SamlClient.Binding.POST).build() // Request to consumer IdP + .login().idp(bc.getIDPAlias()).build() + + .processSamlResponse(SamlClient.Binding.POST) // AuthnRequest to producer IdP + .targetAttributeSamlRequest() + .build() + + .login().user(bc.getUserLogin(), bc.getUserPassword()).build() + + .processSamlResponse(SamlClient.Binding.POST) // Response from producer IdP + .transformDocument(encryptDocument(publicKey, keyEncryptionAlgorithm)) + .build(); + + if (shouldPass) { + // first-broker flow + SAMLDocumentHolder samlResponse = + samlClientBuilder.updateProfile().firstName("a").lastName("b").email(bc.getUserEmail()).build() + .followOneRedirect() + .getSamlResponse(SamlClient.Binding.POST); // Response from consumer IdP + + assertThat(samlResponse, Matchers.notNullValue()); + assertThat(samlResponse.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + + assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(1)); + } else { + samlClientBuilder.executeAndTransform(response -> { + assertThat(response, statusCodeIsHC(Response.Status.BAD_REQUEST)); + return null; + }); + } + + } + } + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java index 33a77e8c38d..3dd8d9a7c5a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java @@ -43,12 +43,9 @@ import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.ProfileAssume; -import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.client.resources.TestingCacheResource; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; -import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.OAuthClient; -import org.keycloak.testsuite.util.WaitUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -150,7 +147,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { cfg.setValidateSignature(true); cfg.setUseJwksUrl(false); - KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.RS256); + KeysMetadataRepresentation.KeyMetadataRepresentation key = org.keycloak.testsuite.util.KeyUtils.findActiveSigningKey(providerRealm(), Algorithm.RS256); cfg.setPublicKeySignatureVerifier(key.getPublicKey()); updateIdentityProvider(idpRep); @@ -185,7 +182,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { rotateKeys(Algorithm.ES256, "ecdsa-generated"); - KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.ES256); + KeysMetadataRepresentation.KeyMetadataRepresentation key = org.keycloak.testsuite.util.KeyUtils.findActiveSigningKey(providerRealm(), Algorithm.ES256); cfg.setPublicKeySignatureVerifier(key.getPublicKey()); updateIdentityProvider(idpRep); @@ -213,7 +210,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { rotateKeys(Algorithm.PS512, "rsa-generated"); - KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.PS512); + KeysMetadataRepresentation.KeyMetadataRepresentation key = org.keycloak.testsuite.util.KeyUtils.findActiveSigningKey(providerRealm(), Algorithm.PS512); cfg.setPublicKeySignatureVerifier(key.getPublicKey()); updateIdentityProvider(idpRep); @@ -267,7 +264,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { cfg.setValidateSignature(true); cfg.setUseJwksUrl(false); - KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm(), Algorithm.RS256); + KeysMetadataRepresentation.KeyMetadataRepresentation key = org.keycloak.testsuite.util.KeyUtils.findActiveSigningKey(providerRealm(), Algorithm.RS256); String pemData = key.getPublicKey(); cfg.setPublicKeySignatureVerifier(pemData); String expectedKeyId = KeyUtils.createKeyId(PemUtils.decodePublicKey(pemData)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java index 58c537a8c0e..debae043a79 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java @@ -46,7 +46,7 @@ public class KcOidcBrokerPrivateKeyJwtTest extends AbstractBrokerTest { public List createProviderClients() { List clientsRepList = super.createProviderClients(); log.info("Update provider clients to accept JWT authentication"); - KeyMetadataRepresentation keyRep = KeyUtils.getActiveSigningKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256); + KeyMetadataRepresentation keyRep = KeyUtils.findActiveSigningKey(adminClient.realm(consumerRealmName()), Algorithm.RS256); for (ClientRepresentation client: clientsRepList) { client.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID); if (client.getAttributes() == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedAssertionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedAssertionTest.java new file mode 100644 index 00000000000..087da016d13 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedAssertionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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.testsuite.broker; + +import org.keycloak.saml.RandomSecret; +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.processing.core.util.XMLEncryptionUtil; +import org.keycloak.testsuite.util.saml.SamlDocumentStepBuilder; +import org.w3c.dom.Node; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.namespace.QName; +import java.security.PublicKey; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI; +import static org.keycloak.testsuite.utils.io.IOUtil.setDocElementAttributeValue; + +public class KcSamlEncryptedAssertionTest extends AbstractKcSamlEncryptedElementsTest { + + @Override + protected SamlDocumentStepBuilder.Saml2DocumentTransformer encryptDocument(PublicKey publicKey, String keyEncryptionAlgorithm) { + return document -> { // Replace Assertion with EncryptedAssertion + Node assertionElement = document.getDocumentElement() + .getElementsByTagNameNS(ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0); + + if (assertionElement == null) { + throw new IllegalStateException("Unable to find assertion in saml response document"); + } + + String samlNSPrefix = assertionElement.getPrefix(); + + // We need to add saml namespace to Assertion + // reason for that is because decryption is performed with assertion element extracted from the original + // document which has definition of saml namespace. After decrypting Assertion element without parent element + // saml namespace is not bound, so we add it + setDocElementAttributeValue(document, samlNSPrefix + ":" + JBossSAMLConstants.ASSERTION.get(), "xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion"); + + try { + + QName encryptedAssertionElementQName = new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), + JBossSAMLConstants.ENCRYPTED_ASSERTION.get(), samlNSPrefix); + + int encryptionKeySize = 128; + + byte[] secret = RandomSecret.createRandomSecret(encryptionKeySize / 8); + SecretKey secretKey = new SecretKeySpec(secret, "AES"); + + // encrypt the Assertion element and replace it with a EncryptedAssertion element. + XMLEncryptionUtil.encryptElement(new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), + JBossSAMLConstants.ASSERTION.get(), samlNSPrefix), document, publicKey, + secretKey, encryptionKeySize, encryptedAssertionElementQName, true, keyEncryptionAlgorithm); + } catch (Exception e) { + throw new ProcessingException("failed to encrypt", e); + } + + assertThat(DocumentUtil.asString(document), containsString(keyEncryptionAlgorithm)); + return document; + }; + } +} + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedIdTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedIdTest.java index a220243238b..1397e2421c9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedIdTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlEncryptedIdTest.java @@ -1,116 +1,68 @@ package org.keycloak.testsuite.broker; -import org.hamcrest.Matchers; -import org.junit.Test; -import org.keycloak.common.util.PemUtils; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.hamcrest.CoreMatchers; import org.keycloak.saml.RandomSecret; import org.keycloak.saml.common.constants.JBossSAMLConstants; -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.util.DocumentUtil; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; -import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.util.XMLEncryptionUtil; -import org.keycloak.testsuite.admin.ApiUtil; -import org.keycloak.testsuite.util.SamlClient; -import org.keycloak.testsuite.util.SamlClientBuilder; -import org.w3c.dom.Document; +import org.keycloak.testsuite.util.saml.SamlDocumentStepBuilder; import org.w3c.dom.Element; import org.w3c.dom.Node; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.xml.namespace.QName; -import java.util.concurrent.atomic.AtomicReference; +import java.security.PublicKey; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.not; import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI; -import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot; -import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_CLIENT_ID_SALES_POST; -import static org.keycloak.testsuite.util.Matchers.isSamlResponse; -public class KcSamlEncryptedIdTest extends AbstractBrokerTest { +public class KcSamlEncryptedIdTest extends AbstractKcSamlEncryptedElementsTest { + @Override - protected BrokerConfiguration getBrokerConfiguration() { - return KcSamlBrokerConfiguration.INSTANCE; - } + protected SamlDocumentStepBuilder.Saml2DocumentTransformer encryptDocument(PublicKey publicKey, String keyEncryptionAlgorithm) { + return document -> { // Replace Subject -> NameID with EncryptedId + Node assertionElement = document.getDocumentElement() + .getElementsByTagNameNS(ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0); - @Test - public void testEncryptedIdIsReadable() throws ConfigurationException, ParsingException, ProcessingException { - createRolesForRealm(bc.consumerRealmName()); + if (assertionElement == null) { + throw new IllegalStateException("Unable to find assertion in saml response document"); + } - AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null); + String samlNSPrefix = assertionElement.getPrefix(); + String username; + try { + QName encryptedIdElementQName = new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.ENCRYPTED_ID.get(), samlNSPrefix); + QName nameIdQName = new QName(ASSERTION_NSURI.get(), + JBossSAMLConstants.NAMEID.get(), samlNSPrefix); - Document doc = SAML2Request.convert(loginRep); + // Add xmlns:saml attribute to NameId element, + // this is necessary as it is decrypted as a separate doc and saml namespace is not know + // unless added to NameId element + Element nameIdElement = DocumentUtil.getElement(document, nameIdQName); + if (nameIdElement == null) { + throw new RuntimeException("Assertion doesn't contain NameId " + DocumentUtil.asString(document)); + } + nameIdElement.setAttribute("xmlns:" + samlNSPrefix, ASSERTION_NSURI.get()); + username = nameIdElement.getTextContent(); - final AtomicReference username = new AtomicReference<>(); - assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(0)); + byte[] secret = RandomSecret.createRandomSecret(128 / 8); + SecretKey secretKey = new SecretKeySpec(secret, "AES"); - SAMLDocumentHolder samlResponse = new SamlClientBuilder() - .authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, SamlClient.Binding.POST).build() // Request to consumer IdP - .login().idp(bc.getIDPAlias()).build() + // encrypt the Assertion element and replace it with a EncryptedAssertion element. + XMLEncryptionUtil.encryptElement(nameIdQName, document, publicKey, + secretKey, 128, encryptedIdElementQName, true, keyEncryptionAlgorithm); + } catch (Exception e) { + throw new ProcessingException("failed to encrypt", e); + } - .processSamlResponse(SamlClient.Binding.POST) // AuthnRequest to producer IdP - .targetAttributeSamlRequest() - .build() - - .login().user(bc.getUserLogin(), bc.getUserPassword()).build() - - .processSamlResponse(SamlClient.Binding.POST) // Response from producer IdP - .transformDocument(document -> { // Replace Subject -> NameID with EncryptedId - Node assertionElement = document.getDocumentElement() - .getElementsByTagNameNS(ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0); - - if (assertionElement == null) { - throw new IllegalStateException("Unable to find assertion in saml response document"); - } - - String samlNSPrefix = assertionElement.getPrefix(); - - try { - QName encryptedIdElementQName = new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.ENCRYPTED_ID.get(), samlNSPrefix); - QName nameIdQName = new QName(ASSERTION_NSURI.get(), - JBossSAMLConstants.NAMEID.get(), samlNSPrefix); - - // Add xmlns:saml attribute to NameId element, - // this is necessary as it is decrypted as a separate doc and saml namespace is not know - // unless added to NameId element - Element nameIdElement = DocumentUtil.getElement(document, nameIdQName); - if (nameIdElement == null) { - throw new RuntimeException("Assertion doesn't contain NameId " + DocumentUtil.asString(document)); - } - nameIdElement.setAttribute("xmlns:" + samlNSPrefix, ASSERTION_NSURI.get()); - username.set(nameIdElement.getTextContent()); - - byte[] secret = RandomSecret.createRandomSecret(128 / 8); - SecretKey secretKey = new SecretKeySpec(secret, "AES"); - - // encrypt the Assertion element and replace it with a EncryptedAssertion element. - XMLEncryptionUtil.encryptElement(nameIdQName, document, PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm(bc.consumerRealmName())).getPublicKey()), - secretKey, 128, encryptedIdElementQName, true); - } catch (Exception e) { - throw new ProcessingException("failed to encrypt", e); - } - - assertThat(DocumentUtil.asString(document), not(containsString(username.get()))); - return document; - }) - .build() - - // first-broker flow - .updateProfile().firstName("a").lastName("b").email(bc.getUserEmail()).build() - .followOneRedirect() - .getSamlResponse(SamlClient.Binding.POST); // Response from consumer IdP - - assertThat(samlResponse, Matchers.notNullValue()); - assertThat(samlResponse.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); - - assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(1)); + String doc = DocumentUtil.asString(document); + assertThat(doc, not(containsString(username))); + assertThat(doc, CoreMatchers.containsString(keyEncryptionAlgorithm)); + return document; + }; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index f757c88c9b4..fa62f91eebc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -15,6 +15,7 @@ import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentExportRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.util.DocumentUtil; @@ -60,7 +61,6 @@ import org.w3c.dom.NodeList; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.broker.BrokerTestConstants.*; import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot; import static org.keycloak.testsuite.util.Matchers.bodyHC; import static org.keycloak.testsuite.util.Matchers.isSamlResponse; @@ -68,12 +68,20 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot; public class KcSamlSignedBrokerTest extends AbstractBrokerTest { - public void withSignedEncryptedAssertions(Runnable testBody, boolean signedDocument, boolean signedAssertion, boolean encryptedAssertion) throws Exception { - String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); - Assert.assertThat(providerCert, Matchers.notNullValue()); - String consumerCert = KeyUtils.getActiveEncKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), JWEConstants.RSA_OAEP).getCertificate(); - Assert.assertThat(consumerCert, Matchers.notNullValue()); + public void withSignedEncryptedAssertions(Runnable testBody, boolean signedDocument, boolean signedAssertion, boolean encryptedAssertion) throws Exception { + + KeysMetadataRepresentation consumerKeysMetadata = adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(); + KeysMetadataRepresentation providerKeysMetadata = adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(); + + String providerSigCert = KeyUtils.findActiveSigningKey(adminClient.realm(bc.providerRealmName()), Algorithm.RS256).getCertificate(); + Assert.assertThat(providerSigCert, Matchers.notNullValue()); + + String consumerEncCert = KeyUtils.findActiveEncryptingKey(adminClient.realm(bc.consumerRealmName()), Algorithm.RSA_OAEP).getCertificate(); + Assert.assertThat(consumerEncCert, Matchers.notNullValue()); + + String consumerSigCert = KeyUtils.findActiveSigningKey(adminClient.realm(bc.consumerRealmName()), Algorithm.RS256).getCertificate(); + Assert.assertThat(consumerSigCert, Matchers.notNullValue()); try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) .setAttribute(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, Boolean.toString(signedAssertion || signedDocument)) @@ -81,13 +89,14 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, Boolean.toString(encryptedAssertion)) .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "false") .setAttribute(SAMLIdentityProviderConfig.ENCRYPTION_ALGORITHM, JWEConstants.RSA_OAEP) - .setAttribute(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert) + .setAttribute(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerSigCert) .update(); Closeable clientUpdater = ClientAttributeUpdater.forClient(adminClient, bc.providerRealmName(), bc.getIDPClientIdInProviderRealm()) .setAttribute(SamlConfigAttributes.SAML_ENCRYPT, Boolean.toString(encryptedAssertion)) - .setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, consumerCert) + .setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, consumerEncCert) .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(signedDocument)) .setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, Boolean.toString(signedAssertion)) + .setAttribute(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, consumerSigCert) .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false") // Do not require client signature .update()) { @@ -251,36 +260,12 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration { - @Override - public RealmRepresentation createProviderRealm() { - RealmRepresentation realm = super.createProviderRealm(); - - realm.setPublicKey(REALM_PUBLIC_KEY); - realm.setPrivateKey(REALM_PRIVATE_KEY); - - return realm; - } - - @Override - public RealmRepresentation createConsumerRealm() { - RealmRepresentation realm = super.createConsumerRealm(); - realm.setId(realm.getRealm()); - - ComponentExportRepresentation signingKey = createKeyRepToRealm(realm,"rsa"); - signingKey.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, REALM_PRIVATE_KEY); - - ComponentExportRepresentation decryptionKey = createKeyRepToRealm(realm, GeneratedRsaEncKeyProviderFactory.ID); - decryptionKey.getConfig().putSingle(Attributes.KEY_USE, KeyUse.ENC.name()); - decryptionKey.getConfig().putSingle(Attributes.ALGORITHM_KEY, JWEConstants.RSA_OAEP); - - return realm; - } @Override public List createProviderClients() { List clientRepresentationList = super.createProviderClients(); - String consumerCert = KeyUtils.getActiveSigningKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String consumerCert = KeyUtils.findActiveSigningKey(adminClient.realm(consumerRealmName()), Algorithm.RS256).getCertificate(); Assert.assertThat(consumerCert, Matchers.notNullValue()); for (ClientRepresentation client : clientRepresentationList) { @@ -307,7 +292,7 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation result = super.setUpIdentityProvider(syncMode); - String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String providerCert = KeyUtils.findActiveSigningKey(adminClient.realm(providerRealmName()), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); Map config = result.getConfig(); @@ -461,10 +446,10 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public void testSignatureDataWhenWantsRequestsSigned() throws Exception { // Verifies that an AuthnRequest contains the KeyInfo/X509Data element when // client AuthnRequest signature is requested - String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String providerCert = KeyUtils.findActiveSigningKey(adminClient.realm(bc.providerRealmName()), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); - String consumerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String consumerCert = KeyUtils.findActiveSigningKey(adminClient.realm(bc.consumerRealmName()), Algorithm.RS256).getCertificate(); Assert.assertThat(consumerCert, Matchers.notNullValue()); try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) @@ -515,17 +500,4 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { .execute(); } } - - protected ComponentExportRepresentation createKeyRepToRealm(RealmRepresentation realmRep, String providerId) { - ComponentExportRepresentation rep = new ComponentExportRepresentation(); - rep.setName(providerId); - rep.setProviderId(providerId); - rep.setConfig(new MultivaluedHashMap<>()); - rep.getConfig().putSingle(Attributes.PRIORITY_KEY, DefaultKeyProviders.DEFAULT_PRIORITY); - if (realmRep.getComponents() == null) { - realmRep.setComponents(new MultivaluedHashMap<>()); - } - realmRep.getComponents().add(KeyProvider.class.getName(), rep); - return rep; - } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSpDescriptorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSpDescriptorTest.java index 8d58f3a1ac8..5f99382eb90 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSpDescriptorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSpDescriptorTest.java @@ -3,36 +3,49 @@ package org.keycloak.testsuite.broker; import com.google.common.collect.ImmutableMap; import org.apache.tools.ant.filters.StringInputStream; +import org.junit.Assert; import org.junit.Test; import org.keycloak.broker.provider.ConfigConstants; import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.broker.saml.mappers.AttributeToRoleMapper; import org.keycloak.broker.saml.mappers.UserAttributeMapper; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyStatus; +import org.keycloak.crypto.KeyUse; import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; +import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; -import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.dom.xmlsec.w3.xmlenc.EncryptionMethodType; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.protocol.saml.SAMLEncryptionAlgorithms; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; -import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater; import java.io.Closeable; import java.io.IOException; import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.xml.crypto.dsig.XMLSignature; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.KeyUtils.generateNewRealmKey; public class KcSamlSpDescriptorTest extends AbstractBrokerTest { @@ -220,14 +233,15 @@ public class KcSamlSpDescriptorTest extends AbstractBrokerTest { String encCert = certs.get("encryption"); Assert.assertNotNull(signingCert); Assert.assertNotNull(encCert); - Assert.assertEquals(signingCert, encCert); + Assert.assertNotEquals(signingCert, encCert); + hasEncAlgorithms(spDescriptor, SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier()); } // Enable signing and encryption and set encryption algorithm. Both keys are present and mapped to different realm key (signing to "rsa-generated"m encryption to "rsa-enc-generated") try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "true") .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") - .setAttribute(SAMLIdentityProviderConfig.ENCRYPTION_ALGORITHM, JWEConstants.RSA_OAEP) + .setAttribute(SAMLIdentityProviderConfig.ENCRYPTION_ALGORITHM, Algorithm.RSA_OAEP) .update()) { spDescriptor = getExportedSamlProvider(); @@ -239,6 +253,48 @@ public class KcSamlSpDescriptorTest extends AbstractBrokerTest { Assert.assertNotNull(signingCert); Assert.assertNotNull(encCert); Assert.assertNotEquals(signingCert, encCert); + hasEncAlgorithms(spDescriptor, SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier()); + } + } + + @Test + public void testEncKeyDescriptors() throws Exception { + SPSSODescriptorType spDescriptor; + + try (AutoCloseable ac1 = generateNewRealmKey(adminClient.realm(bc.consumerRealmName()), KeyUse.ENC, Algorithm.RSA1_5); + AutoCloseable ac2 = generateNewRealmKey(adminClient.realm(bc.consumerRealmName()), KeyUse.ENC, Algorithm.RSA_OAEP_256)) { + + // Test all enc keys are present in metadata + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") + .update()) { + spDescriptor = getExportedSamlProvider(); + hasEncAlgorithms(spDescriptor, + SAMLEncryptionAlgorithms.RSA1_5.getXmlEncIdentifier(), + SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier() + ); + } + + // Specify algorithms for IDP + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") + .setAttribute(SAMLIdentityProviderConfig.ENCRYPTION_ALGORITHM, Algorithm.RSA_OAEP) + .update()) { + spDescriptor = getExportedSamlProvider(); + hasEncAlgorithms(spDescriptor, + SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier() + ); + } + + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") + .setAttribute(SAMLIdentityProviderConfig.ENCRYPTION_ALGORITHM, Algorithm.RSA1_5) + .update()) { + spDescriptor = getExportedSamlProvider(); + hasEncAlgorithms(spDescriptor, + SAMLEncryptionAlgorithms.RSA1_5.getXmlEncIdentifier() + ); + } } } @@ -249,6 +305,16 @@ public class KcSamlSpDescriptorTest extends AbstractBrokerTest { return o.getChoiceType().get(0).getDescriptors().get(0).getSpDescriptor(); } + private void hasEncAlgorithms(SPSSODescriptorType spDescriptor, String... expectedAlgorithms) { + List algorithms = spDescriptor.getKeyDescriptor().stream() + .filter(key -> key.getUse() == KeyTypes.ENCRYPTION) + .map(KeyDescriptorType::getEncryptionMethod) + .flatMap(list -> list.stream().map(EncryptionMethodType::getAlgorithm)) + .collect(Collectors.toList()); + + assertThat(algorithms, containsInAnyOrder(expectedAlgorithms)); + } + // Key is usage ("signing" or "encryption"), Value is string with X509 certificate private Map convertCerts(SPSSODescriptorType spDescriptor) { return spDescriptor.getKeyDescriptor().stream() @@ -257,4 +323,59 @@ public class KcSamlSpDescriptorTest extends AbstractBrokerTest { keyDescriptor -> keyDescriptor.getKeyInfo().getElementsByTagNameNS(XMLSignature.XMLNS, "X509Certificate").item(0).getTextContent())); } + + + //KEYCLOAK-18909 + @Test + public void testKeysExistenceInSpMetadata() throws IOException, ParsingException, URISyntaxException { + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "true") + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true") + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") + .update()) + { + + String spDescriptorString = identityProviderResource.export(null).readEntity(String.class); + SAMLParser parser = SAMLParser.getInstance(); + EntityDescriptorType o = (EntityDescriptorType) parser.parse(new StringInputStream(spDescriptorString)); + SPSSODescriptorType spDescriptor = o.getChoiceType().get(0).getDescriptors().get(0).getSpDescriptor(); + + //the SPSSODescriptor should have at least one KeyDescriptor for encryption and one for Signing + List encKeyDescr = spDescriptor.getKeyDescriptor().stream().filter(k -> KeyTypes.ENCRYPTION.equals(k.getUse())).collect(Collectors.toList()); + List sigKeyDescr = spDescriptor.getKeyDescriptor().stream().filter(k -> KeyTypes.SIGNING.equals(k.getUse())).collect(Collectors.toList()); + + assertTrue(encKeyDescr.size() > 0); + assertTrue(sigKeyDescr.size() > 0); + + //also, the keys should match the realm's dedicated keys for enc and sig + + Set encKeyDescNames = encKeyDescr.stream() + .map(k-> k.getKeyInfo().getElementsByTagName("ds:KeyName").item(0).getTextContent().trim()) + .collect(Collectors.toCollection(HashSet::new)); + + Set sigKeyDescNames = sigKeyDescr.stream() + .map(k-> k.getKeyInfo().getElementsByTagName("ds:KeyName").item(0).getTextContent().trim()) + .collect(Collectors.toCollection(HashSet::new)); + + KeysMetadataRepresentation realmKeysMetadata = adminClient.realm(getBrokerConfiguration().consumerRealmName()).keys().getKeyMetadata(); + + long encMatches = realmKeysMetadata.getKeys().stream() + .filter(k -> KeyStatus.valueOf(k.getStatus()).isActive()) + //.filter(k -> "RSA".equals(k.getType().trim())) + .filter(k -> KeyUse.ENC.equals(k.getUse())) + .filter(k -> encKeyDescNames.contains(k.getKid().trim())) + .count(); + + long sigMatches = realmKeysMetadata.getKeys().stream() + .filter(k -> KeyStatus.valueOf(k.getStatus()).isActive()) + //.filter(k -> "RSA".equals(k.getType().trim())) + .filter(k -> KeyUse.SIG.equals(k.getUse())) + .filter(k -> sigKeyDescNames.contains(k.getKid().trim())) + .count(); + + assertTrue(encMatches > 0); + assertTrue(sigMatches > 0); + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java index 474bfaf72aa..1bf8a752929 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java @@ -77,7 +77,7 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { @Test public void privateKeyOnlyForEnc() throws Exception { - privateKeyOnly(ImportedRsaEncKeyProviderFactory.ID, KeyUse.ENC, JWEConstants.RSA_OAEP); + privateKeyOnly(ImportedRsaEncKeyProviderFactory.ID, KeyUse.ENC, Algorithm.RSA_OAEP); } private void privateKeyOnly(String providerId, KeyUse keyUse, String algorithm) throws Exception { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index d9ceca4b3c0..9bd3bc78ed3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -551,7 +551,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { String expectedMigrationRealmKey = "MIIEpAIBAAKCAQEApt6gCllWkVTZ7fy/oRIx6Bxjt9x3eKKyKGFXvN4iaafrNqpYU9lcqPngWJ9DyXGqUf8RpjPaQWiLWLxjw3xGBqLk2E1/Frb9e/dy8rj//fHGq6bujN1iguzyFwxPGT5Asd7jflRI3qU04M8JE52PArqPhGL2Fn+FiSK5SWRIGm+hVL7Ck/E/tVxM25sFG1/UTQqvrROm4q76TmP8FsyZaTLVf7cCwW2QPIX0N5HTVb3QbBb5KIsk4kKmk/g7uUxS9r42tu533LISzRr5CTyWZAL2XFRuF2RrKdE8gwqkEubw6sDmB2mE0EoPdY1DUhBQgVP/5rwJrCtTsUBR2xdEYQIDAQABAoIBAFbbsNBSOlZBpYJUOmcb8nBQPrOYhXN8tGGCccn0klMOvcdhmcJjdPDbyCQ5Gm7DxJUTwNsTSHsdcNMKlJ9Pk5+msJnKlOl87KrXXbTsCQvlCrWUmb0nCzz9GvJWTOHl3oT3cND0DE4gDksqWR4luCgCdevCGzgQvrBoK6wBD+r578uEW3iw10hnJ0+wnGiw8IvPzE1a9xbY4HD8/QrYdaLxuLb/aC1PDuzrz0cOjnvPkrws5JrbUSnbFygJiOv1z4l2Q00uGIxlHtXdwQBnTZZjVi4vOec2BYSHffgwDYEZIglw1mnrV7y0N1nnPbtJK/cegIkXoBQHXm8Q99TrWMUCgYEA9au86qcwrXZZg5H4BpR5cpy0MSkcKDbA1aRL1cAyTCqJxsczlAtLhFADF+NhnlXj4y7gwDEYWrz064nF73I+ZGicvCiyOy+tCTugTyTGS+XR948ElDMS6PCUUXsotS3dKa0b3c9wd2mxeddTjq/ArfgEVZJ6fE1KtjLt9dtfA+8CgYEAreK3JsvjR5b/Xct28TghYUU7Qnasombb/shqqy8FOMjYUr5OUm/OjNIgoCqhOlE8oQDJ4dOZofNSa7tL+oM8Gmbal+E3fRzxnx/9/EC4QV6sVaPLTIyk7EPfKTcZuzH7+BNZtAziTxJw9d6YJQRbkpg92EZIEoR8iDj2Xs5xrK8CgYEAwMVWwwYX8zT3vn7ukTM2LRH7bsvkVUXJgJqgCwT6Mrv6SmkK9vL5+cPS+Y6pjdW1sRGauBSOGL1Grf/4ug/6F03jFt4UJM8fRyxreU7Q7sNSQ6AMpsGA6BnHODycz7ZCYa59PErG5FyiL4of/cm5Nolz1TXQOPNpWZiTEqVlZC8CgYA4YPbjVF4nuxSnU64H/hwMjsbtAM9uhI016cN0J3W4+J3zDhMU9X1x+Tts0wWdg/N1fGz4lIQOl3cUyRCUc/KL2OdtMS+tmDHbVyMho9ZaE5kq10W2Vy+uDz+O/HeSU12QDK4cC8Vgv+jyPy7zaZtLR6NduUPrBRvfiyCOkr8WrwKBgQCY0h4RCdNFhr0KKLLmJipAtV8wBCGcg1jY1KoWKQswbcykfBKwHbF6EooVqkRW0ITjWB7ZZCf8TnSUxe0NXCUAkVBrhzS4DScgtoSZYOOUaSHgOxpfwgnQ3oYotKi98Yg3IsaLs1j4RuPG5Sp1z6o+ELP1uvr8azyn9YlLa+523Q=="; String realmId = migrationRealm.toRepresentation().getId(); List components = migrationRealm.components().query(realmId, KeyProvider.class.getName()); - assertEquals(3, components.size()); + assertEquals(4, components.size()); components = migrationRealm.components().query(realmId, KeyProvider.class.getName(), "rsa"); assertEquals(1, components.size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeSAML2Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeSAML2Test.java index d1c2d79ca73..fcc18fb6885 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeSAML2Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeSAML2Test.java @@ -72,6 +72,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -334,7 +335,7 @@ public class ClientTokenExchangeSAML2Test extends AbstractKeycloakTest { // Decrypt assertion Document assertionDoc = DocumentUtil.getDocument(assertionXML); - Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, privateKeyFromString(ENCRYPTION_PRIVATE_KEY)); + Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, data -> Collections.singletonList(privateKeyFromString(ENCRYPTION_PRIVATE_KEY))); Assert.assertFalse(AssertionUtil.isSignedElement(assertionElement)); AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement); @@ -382,7 +383,7 @@ public class ClientTokenExchangeSAML2Test extends AbstractKeycloakTest { // Verify assertion Document assertionDoc = DocumentUtil.getDocument(assertionXML); - Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, privateKeyFromString(ENCRYPTION_PRIVATE_KEY)); + Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, data -> Collections.singletonList(privateKeyFromString(ENCRYPTION_PRIVATE_KEY))); Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement)); AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement); Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString())); @@ -698,7 +699,7 @@ public class ClientTokenExchangeSAML2Test extends AbstractKeycloakTest { } private PublicKey publicKeyFromString() { - KeysMetadataRepresentation.KeyMetadataRepresentation keyRep = KeyUtils.getActiveSigningKey(adminClient.realm(TEST).keys().getKeyMetadata(), Algorithm.RS256); + KeysMetadataRepresentation.KeyMetadataRepresentation keyRep = KeyUtils.findActiveSigningKey(adminClient.realm(TEST), Algorithm.RS256); return org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(keyRep.getPublicKey()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 5f92c1eb4a4..d579ba9724e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -1443,7 +1443,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest if (keyId == null) { KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils - .getActiveEncKey(testRealm().keys().getKeyMetadata(), + .findActiveEncryptingKey(testRealm(), Algorithm.PS256); keyId = encKey.getKid(); } @@ -1472,8 +1472,8 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Test public void testRealmPublicKeyEncryptedRequestObjectUsingKid() throws Exception { - KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(), - Algorithm.RS256); + KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.findActiveEncryptingKey(testRealm(), + Algorithm.RSA_OAEP); JWEHeader jweHeader = new JWEHeader(RSA_OAEP, JWEConstants.A128CBC_HS256, null, encKey.getKid()); assertRequestObjectEncryption(jweHeader); } @@ -1529,7 +1529,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest String keyId = jweHeader.getKeyId(); if (keyId == null) { - KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(), + KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.findActiveEncryptingKey(testRealm(), Algorithm.PS256); keyId = encKey.getKid(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java index 68bca4400f3..7db6debcef6 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java @@ -57,6 +57,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.KeyUtils; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; @@ -440,7 +441,7 @@ public class UserInfoTest extends AbstractKeycloakTest { .assertEvent(); // Check signature and content - PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm("test")).getPublicKey()); + PublicKey publicKey = PemUtils.decodePublicKey(KeyUtils.findActiveSigningKey(adminClient.realm("test")).getPublicKey()); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JWT); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ArtifactBindingTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ArtifactBindingTest.java index 4f40fe21e8a..4acaa63d450 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ArtifactBindingTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ArtifactBindingTest.java @@ -159,7 +159,7 @@ public class ArtifactBindingTest extends AbstractSamlTest { assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), not(nullValue())); SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-enc"); - AssertionUtil.decryptAssertion(response, loginResponse, deployment.getDecryptionKey()); + AssertionUtil.decryptAssertion(loginResponse, deployment.getDecryptionKey()); assertThat(loginResponse.getAssertions().get(0).getAssertion(), not(nullValue())); assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), nullValue());