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 + } + } }
