This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/WSS-697 in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit 757a2b5306a587c411e31c31286074ec29a423e4 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Oct 30 05:54:54 2026 +0000 WSS-697 - Don't override an existing OpenSAML configuration in OpenSAMLUtil.initSamlEngine --- .../org/apache/wss4j/common/saml/OpenSAMLUtil.java | 83 +++++++-- .../apache/wss4j/common/saml/OpenSAMLUtilTest.java | 206 ++++++++++++++++++++- 2 files changed, 273 insertions(+), 16 deletions(-) diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java index 368d3bac4..dbca47ede 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java @@ -19,6 +19,11 @@ package org.apache.wss4j.common.saml; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceConfigurationError; + +import javax.xml.XMLConstants; import javax.xml.namespace.QName; import net.shibboleth.shared.xml.impl.BasicParserPool; @@ -26,7 +31,6 @@ import net.shibboleth.shared.xml.ParserPool; import org.apache.wss4j.common.crypto.WSProviderConfig; import org.apache.wss4j.common.ext.WSSecurityException; -import org.opensaml.core.config.Configuration; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.config.provider.MapBasedConfiguration; import org.opensaml.core.xml.XMLObject; @@ -60,6 +64,9 @@ public final class OpenSAMLUtil { private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(OpenSAMLUtil.class); + private static final String DEFER_NODE_EXPANSION_FEATURE = + "http://apache.org/xml/features/dom/defer-node-expansion"; + private static XMLObjectProviderRegistry providerRegistry; private static XMLObjectBuilderFactory builderFactory; private static MarshallerFactory marshallerFactory; @@ -82,31 +89,42 @@ public final class OpenSAMLUtil { LOG.debug("Initializing the opensaml2 library..."); WSProviderConfig.init(); - Configuration configuration = new MapBasedConfiguration(); - ConfigurationService.setConfiguration(configuration); + try { + XMLObjectProviderRegistry existingRegistry = getExistingProviderRegistry(); + if (existingRegistry != null) { + // OpenSAML has already been initialized (e.g. via InitializationService), so keep the + // existing configuration, including the parser pools + LOG.debug("Using the existing OpenSAML configuration"); + providerRegistry = existingRegistry; + } else { + ConfigurationService.setConfiguration(new MapBasedConfiguration()); - providerRegistry = new XMLObjectProviderRegistry(); - configuration.register(XMLObjectProviderRegistry.class, providerRegistry, - ConfigurationService.DEFAULT_PARTITION_NAME); + providerRegistry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, providerRegistry); + } - try { + // Always load the object providers, as an existing initialization may be partial (e.g. in OSGi, + // InitializationService only finds the initializers that are visible to the context classloader) OpenSAMLBootstrap.bootstrap(includeXacml); - SAMLConfiguration samlConfiguration = new SAMLConfiguration(); - - configuration.register(SAMLConfiguration.class, samlConfiguration, - ConfigurationService.DEFAULT_PARTITION_NAME); + if (ConfigurationService.get(SAMLConfiguration.class) == null) { + ConfigurationService.register(SAMLConfiguration.class, new SAMLConfiguration()); + } builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); unmarshallerFactory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory(); try { - configureParserPool(); + if (providerRegistry.getParserPool() == null) { + configureParserPool(); + } // used by org.opensaml.saml.saml2.encryption.Decrypter - configuration.register(DecryptionParserPool.class, new DecryptionParserPool(getParserPool()), - ConfigurationService.DEFAULT_PARTITION_NAME); + if (ConfigurationService.get(DecryptionParserPool.class) == null) { + ConfigurationService.register(DecryptionParserPool.class, + new DecryptionParserPool(createDecryptionParserPool())); + } } catch (Throwable t) { LOG.warn("Unable to bootstrap the parser pool part of the opensaml library " + "- some SAML operations may fail", t); @@ -120,6 +138,17 @@ public final class OpenSAMLUtil { } } + private static XMLObjectProviderRegistry getExistingProviderRegistry() { + try { + return ConfigurationService.get(XMLObjectProviderRegistry.class); + } catch (ServiceConfigurationError e) { + // The lookup of a Configuration implementation via the ServiceLoader failed (e.g. due to + // classloader visibility in OSGi), so treat OpenSAML as not initialized + LOG.debug("Unable to look up an existing OpenSAML configuration", e); + return null; + } + } + private static void configureParserPool() throws Throwable { BasicParserPool pp = new BasicParserPool(); pp.setMaxPoolSize(50); @@ -127,6 +156,32 @@ public final class OpenSAMLUtil { providerRegistry.setParserPool(pp); } + /** + * Create a ParserPool for decryption, configured in the same way as OpenSAML's + * DecryptionParserPoolInitializer. + */ + private static ParserPool createDecryptionParserPool() { + try { + BasicParserPool pp = new BasicParserPool(); + pp.setMaxPoolSize(50); + + // Start with the default (hardened) features, and add the decryption-specific feature. The + // hardening features are set explicitly so that they cannot be lost when the features are replaced. + Map<String, Boolean> features = new HashMap<>(pp.getBuilderFeatures()); + features.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); + features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE); + features.put(DEFER_NODE_EXPANSION_FEATURE, Boolean.FALSE); + pp.setBuilderFeatures(features); + + pp.initialize(); + return pp; + } catch (Throwable t) { + // e.g. the XML parser does not support the feature - fall back to the default parser pool + LOG.warn("Unable to create a decryption-specific parser pool, using the default parser pool instead", t); + return getParserPool(); + } + } + /** * Get the configured ParserPool. * diff --git a/ws-security-common/src/test/java/org/apache/wss4j/common/saml/OpenSAMLUtilTest.java b/ws-security-common/src/test/java/org/apache/wss4j/common/saml/OpenSAMLUtilTest.java index 8cc20b258..3ddbb065e 100644 --- a/ws-security-common/src/test/java/org/apache/wss4j/common/saml/OpenSAMLUtilTest.java +++ b/ws-security-common/src/test/java/org/apache/wss4j/common/saml/OpenSAMLUtilTest.java @@ -19,19 +19,221 @@ package org.apache.wss4j.common.saml; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ServiceLoader; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.validation.Schema; + +import net.shibboleth.shared.xml.ParserPool; +import net.shibboleth.shared.xml.impl.BasicParserPool; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.Initializer; +import org.opensaml.core.config.provider.MapBasedConfiguration; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.xmlsec.config.DecryptionParserPool; +import org.opensaml.xmlsec.signature.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Some tests for the Merlin Crypto provider + * Some tests for OpenSAMLUtil */ public class OpenSAMLUtilTest { + @BeforeEach + public void resetOpenSAML() throws Exception { + // Start each test with neither OpenSAML nor WSS4J initialized + ConfigurationService.setConfiguration(new MapBasedConfiguration()); + Field initialized = OpenSAMLUtil.class.getDeclaredField("samlEngineInitialized"); + initialized.setAccessible(true); + initialized.setBoolean(null, false); + } + @Test public void testInitSamlEngine() throws Exception { OpenSAMLUtil.initSamlEngine(); // https://issues.apache.org/jira/browse/WSS-678 OpenSAML Decrypter initialization failed new org.opensaml.saml.saml2.encryption.Decrypter(null, null, null); + + // The decryption parser pool is distinct from the general parser pool + DecryptionParserPool decryptionParserPool = ConfigurationService.get(DecryptionParserPool.class); + assertNotNull(decryptionParserPool); + assertNotSame(OpenSAMLUtil.getParserPool(), decryptionParserPool.getParserPool()); + assertRejectsDoctype(decryptionParserPool.getParserPool()); + + assertWss4jConfig(); + } + + // https://issues.apache.org/jira/browse/WSS-697 + @Test + public void testExistingOpenSAMLConfigurationIsPreserved() throws Exception { + // Initialize OpenSAML in the same way as InitializationService.initialize(), except for the + // metrics initializer, as metrics-core is excluded from the WSS4J dependencies + for (Initializer initializer : ServiceLoader.load(Initializer.class)) { + if (!initializer.getClass().getName().endsWith(".MetricRegistryInitializer")) { + initializer.init(); + } + } + + ParserPool parserPool = XMLObjectProviderRegistrySupport.getParserPool(); + DecryptionParserPool decryptionParserPool = ConfigurationService.get(DecryptionParserPool.class); + assertNotNull(parserPool); + assertNotNull(decryptionParserPool); + + OpenSAMLUtil.initSamlEngine(); + + assertSame(parserPool, XMLObjectProviderRegistrySupport.getParserPool()); + assertSame(parserPool, OpenSAMLUtil.getParserPool()); + assertSame(decryptionParserPool, ConfigurationService.get(DecryptionParserPool.class)); + + new org.opensaml.saml.saml2.encryption.Decrypter(null, null, null); + + assertWss4jConfig(); + } + + // e.g. in OSGi, InitializationService may only find some of the initializers + @Test + public void testPartialOpenSAMLConfiguration() throws Exception { + XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + BasicParserPool parserPool = new BasicParserPool(); + parserPool.initialize(); + registry.setParserPool(parserPool); + + OpenSAMLUtil.initSamlEngine(); + + assertSame(registry, ConfigurationService.get(XMLObjectProviderRegistry.class)); + assertSame(parserPool, OpenSAMLUtil.getParserPool()); + assertNotNull(XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(Assertion.DEFAULT_ELEMENT_NAME)); + assertNotNull(ConfigurationService.get(DecryptionParserPool.class)); + assertWss4jConfig(); + } + + @Test + public void testDecryptionParserPoolFallback() throws Exception { + String factoryProperty = DocumentBuilderFactory.class.getName(); + String oldFactory = System.getProperty(factoryProperty); + System.setProperty(factoryProperty, NoDeferNodeExpansionDocumentBuilderFactory.class.getName()); + try { + OpenSAMLUtil.initSamlEngine(); + } finally { + if (oldFactory == null) { + System.clearProperty(factoryProperty); + } else { + System.setProperty(factoryProperty, oldFactory); + } + } + + // The XML parser does not support the decryption-specific feature, so the default parser pool is used + DecryptionParserPool decryptionParserPool = ConfigurationService.get(DecryptionParserPool.class); + assertNotNull(decryptionParserPool); + assertSame(OpenSAMLUtil.getParserPool(), decryptionParserPool.getParserPool()); + assertRejectsDoctype(decryptionParserPool.getParserPool()); + + new org.opensaml.saml.saml2.encryption.Decrypter(null, null, null); + } + + private static void assertRejectsDoctype(ParserPool parserPool) { + String xxe = "<!DOCTYPE root [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><root>&xxe;</root>"; + assertThrows(Exception.class, + () -> parserPool.parse(new ByteArrayInputStream(xxe.getBytes(StandardCharsets.UTF_8)))); + } + + private static void assertWss4jConfig() { + assertTrue(XMLObjectProviderRegistrySupport.getUnmarshallerFactory() + .getUnmarshaller(X509Certificate.DEFAULT_ELEMENT_NAME) instanceof WSS4JXSBase64BinaryUnmarshaller); + assertNotNull(XMLObjectProviderRegistrySupport.getBuilderFactory() + .getBuilder(new QName("urn:oasis:xacml:2.0:saml:assertion:schema:os", "XACMLAuthzDecisionStatement"))); + } + + /** + * A DocumentBuilderFactory that does not support the "defer-node-expansion" feature + */ + public static class NoDeferNodeExpansionDocumentBuilderFactory extends DocumentBuilderFactory { + private final DocumentBuilderFactory delegate = DocumentBuilderFactory.newDefaultInstance(); + + @Override + public void setFeature(String name, boolean value) throws ParserConfigurationException { + if (name.endsWith("defer-node-expansion")) { + throw new ParserConfigurationException("Feature not supported: " + name); + } + delegate.setFeature(name, value); + } + + @Override + public boolean getFeature(String name) throws ParserConfigurationException { + return delegate.getFeature(name); + } + + @Override + public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { + return delegate.newDocumentBuilder(); + } + + @Override + public void setAttribute(String name, Object value) { + delegate.setAttribute(name, value); + } + + @Override + public Object getAttribute(String name) { + return delegate.getAttribute(name); + } + + @Override + public void setNamespaceAware(boolean awareness) { + delegate.setNamespaceAware(awareness); + } + + @Override + public void setValidating(boolean validating) { + delegate.setValidating(validating); + } + + @Override + public void setIgnoringElementContentWhitespace(boolean whitespace) { + delegate.setIgnoringElementContentWhitespace(whitespace); + } + + @Override + public void setExpandEntityReferences(boolean expandEntityRef) { + delegate.setExpandEntityReferences(expandEntityRef); + } + + @Override + public void setIgnoringComments(boolean ignoreComments) { + delegate.setIgnoringComments(ignoreComments); + } + + @Override + public void setCoalescing(boolean coalescing) { + delegate.setCoalescing(coalescing); + } + + @Override + public void setSchema(Schema schema) { + delegate.setSchema(schema); + } + + @Override + public void setXIncludeAware(boolean state) { + delegate.setXIncludeAware(state); + } } -} \ No newline at end of file +}
