Use encryption keys rather than sig for crypto in SAML

Closes #13606

Co-authored-by: mhajas <mhajas@redhat.com>
Co-authored-by: hmlnarik <hmlnarik@redhat.com>
This commit is contained in:
laskasn 2022-12-13 12:58:41 +01:00 committed by Michal Hajas
parent 5b626231d9
commit dc8b759c3d
38 changed files with 1035 additions and 350 deletions

View File

@ -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()));
}

View File

@ -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";
}

View File

@ -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);
}

View File

@ -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"),

View File

@ -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,
public static EntityDescriptorType buildSPDescriptor(URI loginBinding, URI logoutBinding, URI assertionEndpoint, URI logoutEndpoint,
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
String entityId, String nameIDPolicyFormat, List<KeyDescriptorType> signingCerts, List<KeyDescriptorType> 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
{

View File

@ -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);

View File

@ -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<EncryptionMethodType> 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());

View File

@ -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<PrivateKey> 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,6 +263,16 @@ public class XMLEncryptionUtil {
Document decryptedDoc = null;
if (encryptedData != null && encryptedKey != null) {
boolean success = false;
final Exception enclosingThrowable = new RuntimeException("Cannot decrypt element in document");
List<PrivateKey> encryptionKeys;
encryptionKeys = decryptionKeyLocator.getKeys(encryptedData);
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();
@ -251,12 +282,19 @@ public class XMLEncryptionUtil {
cipher.init(XMLCipher.DECRYPT_MODE, encryptionKey);
decryptedDoc = cipher.doFinal(documentWithEncryptedElement, encDataElement);
success = true;
break;
} catch (Exception e) {
throw logger.processingError(e);
enclosingThrowable.addSuppressed(e);
}
}
if(decryptedDoc == null){
if (!success) {
throw logger.processingError(enclosingThrowable);
}
}
if (decryptedDoc == null) {
throw logger.nullValueError("decryptedDoc");
}

View File

@ -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());

View File

@ -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());

View File

@ -72,7 +72,7 @@ public class DefaultKeyProviders {
MultivaluedHashMap<String, String> 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<String, String> 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<String, String> 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);
}

View File

@ -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();

View File

@ -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<SAMLIdentityP
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
List<Element> 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<KeyDescriptorType> 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<KeyWrapper> encryptionKeyWrappers = (encAlg != null && !encAlg.trim().isEmpty())
? session.keys().getKeysStream(realm, KeyUse.ENC, encAlg)
: session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256);
List<Element> encryptionKeys = streamForExport(encryptionKeyWrappers, true)
List<KeyDescriptorType> 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<SAMLIdentityP
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPdescriptor(
EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPDescriptor(
authnBinding, authnBinding, endpoint, endpoint,
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
entityId, nameIDPolicyFormat, signingKeys, encryptionKeys);
@ -454,23 +480,6 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
}
private Stream<Element> streamForExport(Stream<KeyWrapper> 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) {

View File

@ -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

View File

@ -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

View File

@ -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:
* <pre>
* {@code
* <xenc:EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element">
* <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
* <ds:KeyInfo>
* <xenc:EncryptedKey>
* <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
* <xenc:CipherData>
* <xenc:CipherValue>
* .....
* </xenc:CipherValue>
* </xenc:CipherData>
* </xenc:EncryptedKey>
* </ds:KeyInfo>
* <xenc:CipherData>
* <xenc:CipherValue>
* ...
* </xenc:CipherValue>
* </xenc:CipherData>
* </xenc:EncryptedData>
* }
* </pre>
*
*/
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<String> getKeyNames(KeyInfo keyInfo) {
List<String> 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<KeyWrapper> 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<PrivateKey> getKeys(EncryptedData encryptedData) {
// Check encryptedData contains keyinfo
KeyInfo keyInfo = encryptedData.getKeyInfo();
if (keyInfo == null) {
throw new IllegalStateException("EncryptedData does not contain KeyInfo");
}
Stream<KeyWrapper> 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<String> 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());
}
}

View File

@ -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<String, SAMLEncryptionAlgorithms> forXMLEncIdentifier = Arrays.stream(values()).collect(Collectors.toMap(SAMLEncryptionAlgorithms::getXmlEncIdentifier, Function.identity()));
private static final Map<String, SAMLEncryptionAlgorithms> 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);
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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<KeysMetadataRepresentation.KeyMetadataRepresentation> findRealmKeys(RealmResource realm, Predicate<KeysMetadataRepresentation.KeyMetadataRepresentation> 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}.
*/

View File

@ -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) {

View File

@ -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("<keycloak-saml-adapter>"));
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("<secure-deployment"));
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()));
}

View File

@ -37,6 +37,7 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import java.security.PublicKey;
@ -71,7 +72,7 @@ public abstract class AbstractGroupTest extends AbstractKeycloakTest {
String accessToken = tokenResponse.getAccessToken();
String refreshToken = tokenResponse.getRefreshToken();
PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm("test")).getPublicKey());
PublicKey publicKey = PemUtils.decodePublicKey(KeyUtils.findActiveSigningKey(adminClient.realm("test")).getPublicKey());
AccessToken accessTokenRepresentation = RSATokenVerifier.verifyToken(accessToken, publicKey, getAuthServerContextRoot() + "/auth/realms/test");

View File

@ -0,0 +1,165 @@
/*
* 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.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.protocol.saml.SAMLEncryptionAlgorithms;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
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.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.testsuite.util.saml.SamlDocumentStepBuilder;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
import java.security.PublicKey;
import java.util.concurrent.atomic.AtomicReference;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.keycloak.broker.saml.SAMLEndpoint.ENCRYPTION_DEPRECATED_MODE_PROPERTY;
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;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
public abstract class AbstractKcSamlEncryptedElementsTest extends AbstractBrokerTest {
private String encProviderId;
private String sigProviderId;
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcSamlBrokerConfiguration.INSTANCE;
}
@Before
public void setupKeys() {
sigProviderId = KeyUtils.findActiveSigningKey(adminClient.realm(bc.consumerRealmName())).getProviderId();
encProviderId = KeyUtils.findActiveEncryptingKey(adminClient.realm(bc.consumerRealmName()), Algorithm.RSA_OAEP).getProviderId();
assertThat(sigProviderId, not(equalTo(encProviderId)));
}
@Test
public void testEncryptedElementIsReadable() throws ConfigurationException, ParsingException, ProcessingException {
KeysMetadataRepresentation.KeyMetadataRepresentation activeEncryptingKey = KeyUtils.findActiveEncryptingKey(adminClient.realm(bc.consumerRealmName()), Algorithm.RSA_OAEP);
assertThat(activeEncryptingKey.getProviderId(), equalTo(encProviderId));
sendDocumentWithEncryptedElement(PemUtils.decodePublicKey(activeEncryptingKey.getPublicKey()), SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier(), true);
}
@Test
public void testSignatureKeyEncryptedElementIsNotReadableWithoutDeprecatedMode() throws ConfigurationException, ParsingException, ProcessingException {
KeysMetadataRepresentation.KeyMetadataRepresentation activeSignatureKey = KeyUtils.findActiveSigningKey(adminClient.realm(bc.consumerRealmName()));
assertThat(activeSignatureKey.getProviderId(), equalTo(sigProviderId));
sendDocumentWithEncryptedElement(PemUtils.decodePublicKey(activeSignatureKey.getPublicKey()), SAMLEncryptionAlgorithms.RSA_OAEP.getXmlEncIdentifier(), false);
}
@Test
public void testEncryptedElementIsReadableInDeprecatedMode() throws ConfigurationException, ParsingException, ProcessingException {
try {
// Set flag that enabled deprecated mode for encryption
testingClient.server().run(session -> {
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<String> 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;
});
}
}
}

View File

@ -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));

View File

@ -46,7 +46,7 @@ public class KcOidcBrokerPrivateKeyJwtTest extends AbstractBrokerTest {
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> 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) {

View File

@ -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;
};
}
}

View File

@ -1,69 +1,30 @@
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;
}
@Test
public void testEncryptedIdIsReadable() 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<String> username = new AtomicReference<>();
assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(0));
SAMLDocumentHolder samlResponse = 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(document -> { // Replace Subject -> NameID with EncryptedId
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);
@ -72,7 +33,7 @@ public class KcSamlEncryptedIdTest extends AbstractBrokerTest {
}
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(),
@ -86,31 +47,22 @@ public class KcSamlEncryptedIdTest extends AbstractBrokerTest {
throw new RuntimeException("Assertion doesn't contain NameId " + DocumentUtil.asString(document));
}
nameIdElement.setAttribute("xmlns:" + samlNSPrefix, ASSERTION_NSURI.get());
username.set(nameIdElement.getTextContent());
username = 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);
XMLEncryptionUtil.encryptElement(nameIdQName, document, publicKey,
secretKey, 128, encryptedIdElementQName, true, keyEncryptionAlgorithm);
} catch (Exception e) {
throw new ProcessingException("failed to encrypt", e);
}
assertThat(DocumentUtil.asString(document), not(containsString(username.get())));
String doc = DocumentUtil.asString(document);
assertThat(doc, not(containsString(username)));
assertThat(doc, CoreMatchers.containsString(keyEncryptionAlgorithm));
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));
};
}
}

View File

@ -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<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> 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<String, String> 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;
}
}

View File

@ -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<String> 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<String, String> 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<KeyDescriptorType> encKeyDescr = spDescriptor.getKeyDescriptor().stream().filter(k -> KeyTypes.ENCRYPTION.equals(k.getUse())).collect(Collectors.toList());
List<KeyDescriptorType> 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<String> encKeyDescNames = encKeyDescr.stream()
.map(k-> k.getKeyInfo().getElementsByTagName("ds:KeyName").item(0).getTextContent().trim())
.collect(Collectors.toCollection(HashSet::new));
Set<String> 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);
}
}
}

View File

@ -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 {

View File

@ -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<ComponentRepresentation> 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());

View File

@ -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());
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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());