From b20c2593e99fc6de384af64554ae8266b3323998 Mon Sep 17 00:00:00 2001 From: gtully Date: Fri, 26 Jul 2019 10:34:55 +0100 Subject: [PATCH] ARTEMIS-2433 add ExternalCertificateLoginModule to surface a SASL EXTERNAL identity (subjectDN) to JAAS. --- .../jaas/ExternalCertificateLoginModule.java | 109 +++++++++++ .../core/security/jaas/LDAPLoginModule.java | 5 +- docs/user-manual/en/security.md | 24 +++ .../amqp/JMSSaslExternalLDAPTest.java | 181 ++++++++++++++++++ .../src/test/resources/AMQauth.ldif | 9 +- .../src/test/resources/login.config | 20 ++ 6 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java new file mode 100644 index 0000000000..81d34c97b0 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.spi.core.security.jaas; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import javax.security.cert.X509Certificate; +import java.io.IOException; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.jboss.logging.Logger; + +/** + * A LoginModule that propagates TLS certificates subject DN as a UserPrincipal. + */ +public class ExternalCertificateLoginModule implements LoginModule { + + private static final Logger logger = Logger.getLogger(ExternalCertificateLoginModule.class); + + private CallbackHandler callbackHandler; + private Subject subject; + private String userName; + + private final Set principals = new HashSet<>(); + + @Override + public void initialize(Subject subject, + CallbackHandler callbackHandler, + Map sharedState, + Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + Callback[] callbacks = new Callback[1]; + + callbacks[0] = new CertificateCallback(); + try { + callbackHandler.handle(callbacks); + } catch (IOException ioe) { + throw new LoginException(ioe.getMessage()); + } catch (UnsupportedCallbackException uce) { + throw new LoginException("Unable to obtain client certificates: " + uce.getMessage()); + } + + X509Certificate[] certificates = ((CertificateCallback) callbacks[0]).getCertificates(); + if (certificates != null && certificates.length > 0 && certificates[0] != null) { + userName = certificates[0].getSubjectDN().getName(); + } + + logger.debug("Certificates: " + Arrays.toString(certificates) + ", userName: " + userName); + return userName != null; + } + + @Override + public boolean commit() throws LoginException { + if (userName != null) { + principals.add(new UserPrincipal(userName)); + subject.getPrincipals().addAll(principals); + } + + clear(); + logger.debug("commit"); + return true; + } + + @Override + public boolean abort() throws LoginException { + clear(); + logger.debug("abort"); + return true; + } + + @Override + public boolean logout() { + subject.getPrincipals().removeAll(principals); + principals.clear(); + logger.debug("logout"); + return true; + } + + private void clear() { + userName = null; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java index a098ccddb9..b56fde8116 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -308,8 +308,9 @@ public class LDAPLoginModule implements LoginModule { throw ex; } - if (!isLoginPropertySet(USER_SEARCH_MATCHING)) - return dn; + if (!isLoginPropertySet(USER_SEARCH_MATCHING)) { + return username; + } userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING)); userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue(); diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 2263f204a6..6a45271f01 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -910,6 +910,30 @@ users=system,user guests=guest ``` + +#### Krb5LoginModule + +The Kerberos login module is used to propagate a validated SASL GSSAPI kerberos token +identity into a validated JAAS UserPrincipal. This allows subsequent login modules to +do role mapping for the kerberos identity. + +``` +org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required + ; +``` + +#### ExternalCertificateLoginModule + +The external certificate login module is used to propagate a validated TLS client +certificate's subjectDN into a JAAS UserPrincipal. This allows subsequent login modules to +do role mapping for the TLS client certificate. + +``` +org.apache.activemq.artemis.spi.core.security.jaas.ExternalCertificateLoginModule required + ; +``` + + The simplest way to make the login configuration available to JAAS is to add the directory containing the file, `login.config`, to your CLASSPATH. diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java new file mode 100644 index 0000000000..6320b4fc82 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.tests.integration.amqp; + +import javax.jms.Connection; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import java.io.File; +import java.lang.management.ManagementFactory; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; +import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory; +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.security.Role; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ActiveMQServers; +import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.apache.activemq.artemis.utils.RandomUtil; +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.apache.directory.server.core.integ.FrameworkRunner; +import org.apache.qpid.jms.JmsConnectionFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; + +@RunWith(FrameworkRunner.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)}) +@ApplyLdifFiles("AMQauth.ldif") +public class JMSSaslExternalLDAPTest extends AbstractLdapTestUnit { + + static { + String path = System.getProperty("java.security.auth.login.config"); + if (path == null) { + URL resource = JMSSaslExternalTest.class.getClassLoader().getResource("login.config"); + if (resource != null) { + path = resource.getFile(); + System.setProperty("java.security.auth.login.config", path); + } + } + } + + private ActiveMQServer server; + private final boolean debug = false; + + public static final String TARGET_TMP = "./target/tmp"; + + public JMSSaslExternalLDAPTest() { + super(); + File parent = new File(TARGET_TMP); + parent.mkdirs(); + temporaryFolder = new TemporaryFolder(parent); + } + + @Rule + public TemporaryFolder temporaryFolder; + private String testDir; + + @Before + public void setUp() throws Exception { + + testDir = temporaryFolder.getRoot().getAbsolutePath(); + + if (debug) { + for (java.util.logging.Logger logger : new java.util.logging.Logger[] {java.util.logging.Logger.getLogger("javax.security.sasl"), java.util.logging.Logger.getLogger("org.apache.qpid.proton")}) { + logger.setLevel(java.util.logging.Level.FINEST); + logger.addHandler(new java.util.logging.ConsoleHandler()); + for (java.util.logging.Handler handler : logger.getHandlers()) { + handler.setLevel(java.util.logging.Level.FINEST); + } + } + } + } + + + @Before + public void startServer() throws Exception { + + ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("SaslExternalPlusLdap"); + Configuration configuration = new ConfigurationImpl().setSecurityEnabled(true).addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getCanonicalName())).setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir, 0, false)).setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0, false)).setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0, false)).setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir, 0, false)); + server = ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false); + + + Map params = new HashMap<>(); + params.put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, "keystore1.jks"); + params.put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, "changeit"); + params.put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, "truststore.jks"); + params.put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, "changeit"); + params.put(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, true); + + Map extraParams = new HashMap<>(); + extraParams.put("saslMechanisms", "EXTERNAL"); + + server.getConfiguration().addAcceptorConfiguration(new TransportConfiguration(NettyAcceptorFactory.class.getCanonicalName(), params, "netty", extraParams)); + + // role mapping via CertLogin - TextFileCertificateLoginModule + final String roleName = "widgets"; + Role role = new Role(roleName, true, true, true, true, true, true, true, true, true, true); + Set roles = new HashSet<>(); + roles.add(role); + server.getSecurityRepository().addMatch("TEST", roles); + + server.start(); + } + + @After + public void stopServer() throws Exception { + server.stop(); + } + + @Test(timeout = 600000) + public void testRoundTrip() throws Exception { + + final String keystore = this.getClass().getClassLoader().getResource("client_not_revoked.jks").getFile(); + final String truststore = this.getClass().getClassLoader().getResource("truststore.jks").getFile(); + + String connOptions = "?amqp.saslMechanisms=EXTERNAL" + "&" + + "transport.trustStoreLocation=" + truststore + "&" + + "transport.trustStorePassword=changeit" + "&" + + "transport.keyStoreLocation=" + keystore + "&" + + "transport.keyStorePassword=changeit" + "&" + + "transport.verifyHost=false"; + + JmsConnectionFactory factory = new JmsConnectionFactory(new URI("amqps://localhost:" + 61616 + connOptions)); + Connection connection = factory.createConnection("client", null); + connection.start(); + + try { + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + javax.jms.Queue queue = session.createQueue("TEST"); + MessageConsumer consumer = session.createConsumer(queue); + MessageProducer producer = session.createProducer(queue); + + final String text = RandomUtil.randomString(); + producer.send(session.createTextMessage(text)); + + TextMessage m = (TextMessage) consumer.receive(1000); + assertNotNull(m); + assertEquals(text, m.getText()); + + } finally { + connection.close(); + } + } + +} diff --git a/tests/integration-tests/src/test/resources/AMQauth.ldif b/tests/integration-tests/src/test/resources/AMQauth.ldif index 70e11f3dd1..9036dfe078 100755 --- a/tests/integration-tests/src/test/resources/AMQauth.ldif +++ b/tests/integration-tests/src/test/resources/AMQauth.ldif @@ -116,4 +116,11 @@ dn: cn=admin,uid=activemq.management,ou=queues,ou=destinations,o=ActiveMQ,ou=sys objectclass: groupOfUniqueNames objectclass: top cn: admin -uniquemember: uid=role1 \ No newline at end of file +uniquemember: uid=role1 + +## group with member identified just by DN from SASL external tls certificate subject DN +dn: cn=widgets,ou=system +cn: widgets +member: uid=O=Internet Widgits Pty Ltd,C=AU,ST=Some-State,CN=lakalkalaoioislkxn +objectClass: groupOfNames +objectClass: top \ No newline at end of file diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config index fb7020572f..a70836b126 100644 --- a/tests/integration-tests/src/test/resources/login.config +++ b/tests/integration-tests/src/test/resources/login.config @@ -176,6 +176,26 @@ Krb5Plus { org.apache.activemq.jaas.properties.role="dual-authentication-roles.properties"; }; +SaslExternalPlusLdap { + + org.apache.activemq.artemis.spi.core.security.jaas.ExternalCertificateLoginModule required + debug=true; + + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword=secret + connectionProtocol=s + authentication=simple + authenticateUser=false + roleBase="ou=system" + roleName=cn + roleSearchMatching="(member=uid={1})" + ; +}; + Krb5PlusLdap { org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required