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 <[email protected]>
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
+        }
+    }
 }

Reply via email to