This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 177afac4f22 Restrict default SAML encryption algorithms (#3434)
177afac4f22 is described below
commit 177afac4f22f6b6b9a6f19bc62c8023aecfd4fd0
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 3 09:27:57 2026 +0100
Restrict default SAML encryption algorithms (#3434)
---
.../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
+ }
+ }
}