You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/09/02 08:48:21 UTC
(cxf) branch coheigea/saml-encryption-algorithms created (now 9adae2de069)
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a change to branch coheigea/saml-encryption-algorithms
in repository https://gitbox.apache.org/repos/asf/cxf.git
at 9adae2de069 Restrict default SAML encryption algorithms
This branch includes the following new commits:
new 9adae2de069 Restrict default SAML encryption algorithms
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(cxf) 01/01: Restrict default SAML encryption algorithms
Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch coheigea/saml-encryption-algorithms
in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 9adae2de06970b2485f3e7b5db9bb20be038d680
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Wed Sep 2 09:47:56 2026 +0100
Restrict default SAML encryption algorithms
---
.../saml/sso/SAMLProtocolResponseValidator.java | 56 +++++++++-
.../saml/sso/SAMLResponseValidatorTest.java | 118 +++++++++++++++++++++
2 files changed, 169 insertions(+), 5 deletions(-)
diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLProtocolResponseValidator.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLProtocolResponseValidator.java
index aa3c79005e5..26d64a6cfc2 100644
--- a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLProtocolResponseValidator.java
+++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLProtocolResponseValidator.java
@@ -25,6 +25,7 @@ import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Arrays;
+import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -93,6 +94,25 @@ public class SAMLProtocolResponseValidator {
*/
private int futureTTL = 60;
+ /**
+ * The key transport algorithms accepted when decrypting an EncryptedKey.
+ * RSA-1.5 is deliberately not accepted by default: allowing an
+ * attacker-chosen PKCS#1 v1.5 transport would turn this
+ * pre-authentication endpoint into a Bleichenbacher decryption oracle
+ * against the SP private key.
+ */
+ private Collection<String> allowedKeyTransportAlgorithms =
+ Arrays.asList(XMLCipher.RSA_OAEP, XMLCipher.RSA_OAEP_11);
+
+ /**
+ * The content encryption algorithms accepted when decrypting an
+ * EncryptedData. Only AEAD (AES-GCM) algorithms are accepted by default;
+ * unauthenticated CBC modes expose a padding oracle against captured
+ * assertions and have to be enabled explicitly for legacy IdPs.
+ */
+ private Collection<String> allowedContentEncryptionAlgorithms =
+ Arrays.asList(XMLCipher.AES_128_GCM, XMLCipher.AES_192_GCM, XMLCipher.AES_256_GCM);
+
/**
* Validate a SAML 2 Protocol Response
* @param samlResponse
@@ -452,10 +472,22 @@ public class SAMLProtocolResponseValidator {
throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity");
}
- // now start decrypting
+ // now start decrypting. Only allow-listed algorithms may reach the
+ // private key or the content decryption - both algorithm identifiers
+ // are attacker-supplied
String keyEncAlgo = getEncodingMethodAlgorithm(encKeyElement);
+ if (!allowedKeyTransportAlgorithms.contains(keyEncAlgo)) {
+ LOG.warning("The Key Transport Algorithm " + keyEncAlgo + " is not allowed");
+ throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity");
+ }
String digestAlgo = getDigestMethodAlgorithm(encKeyElement);
+ String symKeyAlgo = getEncodingMethodAlgorithm(encryptedDataDOM);
+ if (!allowedContentEncryptionAlgorithms.contains(symKeyAlgo)) {
+ LOG.warning("The Content Encryption Algorithm " + symKeyAlgo + " is not allowed");
+ throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity");
+ }
+
Element cipherValue = getNode(encKeyElement, WSS4JConstants.ENC_NS, "CipherValue", 0);
if (cipherValue == null) {
LOG.warning("CipherValue element is not available");
@@ -476,7 +508,7 @@ public class SAMLProtocolResponseValidator {
}
Cipher cipher =
EncryptionUtils.initCipherWithKey(keyEncAlgo, digestAlgo, Cipher.DECRYPT_MODE, key);
- final byte[] decryptedBytes;
+ byte[] decryptedBytes;
try {
byte[] encryptedBytes = Base64Utility.decode(cipherValue.getTextContent().trim());
decryptedBytes = cipher.doFinal(encryptedBytes);
@@ -484,12 +516,14 @@ public class SAMLProtocolResponseValidator {
LOG.log(Level.FINE, "Base64 decoding has failed", ex);
throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity");
} catch (Exception ex) {
+ // Substitute a randomly generated symmetric key and carry on, so that
+ // an EncryptedKey decryption failure cannot be distinguished - in
+ // behavior or in timing - from a content decryption failure. This is
+ // the same mitigation WSS4J applies against Bleichenbacher attacks.
LOG.log(Level.FINE, "Encrypted key can not be decrypted", ex);
- throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "invalidSAMLsecurity");
+ decryptedBytes = generateRandomSymmetricKey(symKeyAlgo);
}
- String symKeyAlgo = getEncodingMethodAlgorithm(encryptedDataDOM);
-
final byte[] decryptedPayload;
try {
decryptedPayload = decryptPayload(encryptedDataDOM, decryptedBytes, symKeyAlgo);
@@ -591,6 +625,18 @@ public class SAMLProtocolResponseValidator {
}
}
+ private static byte[] generateRandomSymmetricKey(String symEncAlgo) throws WSSecurityException {
+ return KeyUtils.getKeyGenerator(symEncAlgo).generateKey().getEncoded();
+ }
+
+ public void setAllowedKeyTransportAlgorithms(Collection<String> allowedKeyTransportAlgorithms) {
+ this.allowedKeyTransportAlgorithms = allowedKeyTransportAlgorithms;
+ }
+
+ public void setAllowedContentEncryptionAlgorithms(Collection<String> allowedContentEncryptionAlgorithms) {
+ this.allowedContentEncryptionAlgorithms = allowedContentEncryptionAlgorithms;
+ }
+
public void setKeyInfoMustBeAvailable(boolean keyInfoMustBeAvailable) {
this.keyInfoMustBeAvailable = keyInfoMustBeAvailable;
}
diff --git a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLResponseValidatorTest.java b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLResponseValidatorTest.java
index 1b4bb80b52b..0eb01579334 100644
--- a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLResponseValidatorTest.java
+++ b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLResponseValidatorTest.java
@@ -27,6 +27,9 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -46,15 +49,24 @@ import org.apache.wss4j.common.saml.bean.SubjectConfirmationDataBean;
import org.apache.wss4j.common.saml.builder.SAML2Constants;
import org.apache.wss4j.common.util.Loader;
import org.apache.wss4j.dom.engine.WSSConfig;
+import org.apache.xml.security.encryption.XMLCipher;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.xmlsec.encryption.EncryptedData;
+import org.opensaml.xmlsec.encryption.EncryptedKey;
+import org.opensaml.xmlsec.encryption.EncryptionMethod;
+import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
+import org.opensaml.xmlsec.encryption.impl.EncryptedKeyBuilder;
+import org.opensaml.xmlsec.encryption.impl.EncryptionMethodBuilder;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.Signature;
+import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import static org.junit.Assert.assertNotNull;
@@ -189,6 +201,36 @@ public class SAMLResponseValidatorTest {
}
}
+ @org.junit.Test
+ public void testEncryptedAssertionRejectsRSA15KeyTransport() throws Exception {
+ Response response = createEncryptedResponse(XMLCipher.RSA_v1dot5, XMLCipher.AES_128_GCM);
+
+ assertEncryptedAssertionRejected(response);
+ }
+
+ @org.junit.Test
+ public void testEncryptedAssertionRejectsCBCContentEncryption() throws Exception {
+ Response response = createEncryptedResponse(XMLCipher.RSA_OAEP, XMLCipher.AES_128);
+
+ assertEncryptedAssertionRejected(response);
+ }
+
+ @org.junit.Test
+ public void testEncryptedAssertionAllowsDefaultAlgorithms() throws Exception {
+ Logger logger = Logger.getLogger(SAMLProtocolResponseValidator.class.getName());
+ CipherValueWarningHandler handler = new CipherValueWarningHandler();
+ logger.addHandler(handler);
+ try {
+ assertEncryptedAssertionRejected(
+ createEncryptedResponse(XMLCipher.RSA_OAEP, XMLCipher.AES_128_GCM)
+ );
+ } finally {
+ logger.removeHandler(handler);
+ }
+
+ assertTrue(handler.cipherValueWarningLogged);
+ }
+
@org.junit.Test
public void testResponseSignedAssertion() throws Exception {
Document doc = DOMUtils.createDocument();
@@ -851,4 +893,80 @@ public class SAMLResponseValidatorTest {
return (Response)OpenSAMLUtil.fromDom(policyElement);
}
+
+ private Response createEncryptedResponse(String keyTransportAlgorithm, String contentEncryptionAlgorithm)
+ throws Exception {
+ Document doc = DOMUtils.createDocument();
+ Status status =
+ SAML2PResponseComponentBuilder.createStatus(
+ SAMLProtocolResponseValidator.SAML2_STATUSCODE_SUCCESS, null
+ );
+ Response response =
+ SAML2PResponseComponentBuilder.createSAMLResponse(
+ "http://cxf.apache.org/saml", "http://cxf.apache.org/issuer", status
+ );
+
+ EncryptionMethod keyTransportMethod = new EncryptionMethodBuilder().buildObject();
+ keyTransportMethod.setAlgorithm(keyTransportAlgorithm);
+ EncryptedKey encryptedKey = new EncryptedKeyBuilder().buildObject();
+ encryptedKey.setEncryptionMethod(keyTransportMethod);
+
+ KeyInfo keyInfo = new KeyInfoBuilder().buildObject();
+ keyInfo.getEncryptedKeys().add(encryptedKey);
+
+ EncryptionMethod contentEncryptionMethod = new EncryptionMethodBuilder().buildObject();
+ contentEncryptionMethod.setAlgorithm(contentEncryptionAlgorithm);
+ EncryptedData encryptedData = new EncryptedDataBuilder().buildObject();
+ encryptedData.setEncryptionMethod(contentEncryptionMethod);
+ encryptedData.setKeyInfo(keyInfo);
+
+ org.opensaml.saml.saml2.core.EncryptedAssertion encryptedAssertion =
+ new EncryptedAssertionBuilder().buildObject();
+ encryptedAssertion.setEncryptedData(encryptedData);
+ response.getEncryptedAssertions().add(encryptedAssertion);
+
+ Element responseElement = OpenSAMLUtil.toDom(response, doc);
+ doc.appendChild(responseElement);
+ return (Response)OpenSAMLUtil.fromDom(responseElement);
+ }
+
+ private void assertEncryptedAssertionRejected(Response response) throws Exception {
+ Crypto issuerCrypto = createAliceCrypto();
+
+ try {
+ new SAMLProtocolResponseValidator().validateSamlResponse(
+ response, issuerCrypto, new KeystorePasswordCallback()
+ );
+ fail("Expected failure on a disallowed encrypted assertion algorithm");
+ } catch (WSSecurityException ex) {
+ // expected
+ }
+ }
+
+ private Crypto createAliceCrypto() throws Exception {
+ Crypto issuerCrypto = new Merlin();
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ ClassLoader loader = Loader.getClassLoader(SAMLResponseValidatorTest.class);
+ InputStream input = Merlin.loadInputStream(loader, "alice.jks");
+ keyStore.load(input, "password".toCharArray());
+ ((Merlin)issuerCrypto).setKeyStore(keyStore);
+ issuerCrypto.setDefaultX509Identifier("alice");
+ return issuerCrypto;
+ }
+
+ private static final class CipherValueWarningHandler extends Handler {
+ private boolean cipherValueWarningLogged;
+
+ public void publish(LogRecord record) {
+ cipherValueWarningLogged |= "CipherValue element is not available".equals(record.getMessage());
+ }
+
+ public void flush() {
+ // nothing to flush
+ }
+
+ public void close() {
+ // nothing to close
+ }
+ }
}