From 1a88ac6ac383eccf8a026368c894f8f21646cd3c Mon Sep 17 00:00:00 2001 From: "Hiram R. Chirino" Date: Mon, 18 Sep 2006 22:43:24 +0000 Subject: [PATCH] Commiting awesome patch by Sepand Mavandadi to add ActiveMQ support for SSL authentication and authorization: https://issues.apache.org/activemq/browse/AMQ-912 git-svn-id: https://svn.apache.org/repos/asf/incubator/activemq/trunk@447608 13f79535-47bb-0310-9956-ffa450edef68 --- .../ActiveMQSslConnectionFactory.java | 89 +++++++ .../activemq/broker/SslBrokerService.java | 119 +++++++++ .../JaasAuthenticationPlugin.java.orig | 88 +++++++ .../JaasCertificateAuthenticationBroker.java | 120 ++++++++++ .../JaasCertificateAuthenticationPlugin.java | 36 +++ .../activemq/transport/tcp/SslTransport.java | 110 +++++++++ .../transport/tcp/SslTransportServer.java | 127 ++++++++++ .../apache/activemq/broker/StubBroker.java | 226 ++++++++++++++++++ ...asCertificateAuthenticationBrokerTest.java | 206 ++++++++++++++++ .../StubDoNothingCallbackHandler.java | 32 +++ .../security/StubJaasConfiguration.java | 38 +++ .../activemq/security/StubLoginModule.java | 93 +++++++ .../security/StubSecurityContext.java | 31 +++ .../tcp/SslTransportFactoryTest.java | 129 ++++++++++ .../transport/tcp/SslTransportServerTest.java | 94 ++++++++ .../transport/tcp/SslTransportTest.java | 106 ++++++++ .../transport/tcp/StubSSLServerSocket.java | 98 ++++++++ .../transport/tcp/StubSSLSession.java | 135 +++++++++++ .../activemq/transport/tcp/StubSSLSocket.java | 127 ++++++++++ .../transport/tcp/StubSSLSocketFactory.java | 59 +++++ .../transport/tcp/StubSslTransport.java | 52 ++++ .../transport/tcp/StubX509Certificate.java | 166 +++++++++++++ .../activemq/jaas/CertificateCallback.java | 52 ++++ .../activemq/jaas/CertificateLoginModule.java | 191 +++++++++++++++ .../jaas/JaasCertificateCallbackHandler.java | 71 ++++++ .../activemq/jaas/JassCredentialCallback.java | 63 +++++ .../jaas/TextFileCertificateLoginModule.java | 140 +++++++++++ .../jaas/CertificateLoginModuleTest.java | 144 +++++++++++ .../jaas/StubCertificateLoginModule.java | 48 ++++ 29 files changed, 2990 insertions(+) create mode 100644 activemq-core/src/main/java/org/apache/activemq/ActiveMQSslConnectionFactory.java create mode 100644 activemq-core/src/main/java/org/apache/activemq/broker/SslBrokerService.java create mode 100644 activemq-core/src/main/java/org/apache/activemq/security/JaasAuthenticationPlugin.java.orig create mode 100644 activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationBroker.java create mode 100644 activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationPlugin.java create mode 100644 activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java create mode 100644 activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransportServer.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/broker/StubBroker.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/security/JaasCertificateAuthenticationBrokerTest.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/security/StubDoNothingCallbackHandler.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/security/StubJaasConfiguration.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/security/StubLoginModule.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/security/StubSecurityContext.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportFactoryTest.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportServerTest.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportTest.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLServerSocket.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSession.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocket.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocketFactory.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSslTransport.java create mode 100644 activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubX509Certificate.java create mode 100644 activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateCallback.java create mode 100644 activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateLoginModule.java create mode 100644 activemq-jaas/src/main/java/org/apache/activemq/jaas/JaasCertificateCallbackHandler.java create mode 100644 activemq-jaas/src/main/java/org/apache/activemq/jaas/JassCredentialCallback.java create mode 100644 activemq-jaas/src/main/java/org/apache/activemq/jaas/TextFileCertificateLoginModule.java create mode 100644 activemq-jaas/src/test/java/org/apache/activemq/jaas/CertificateLoginModuleTest.java create mode 100644 activemq-jaas/src/test/java/org/apache/activemq/jaas/StubCertificateLoginModule.java diff --git a/activemq-core/src/main/java/org/apache/activemq/ActiveMQSslConnectionFactory.java b/activemq-core/src/main/java/org/apache/activemq/ActiveMQSslConnectionFactory.java new file mode 100644 index 0000000000..575e432397 --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/ActiveMQSslConnectionFactory.java @@ -0,0 +1,89 @@ +/** + * + * 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; + +import org.apache.activemq.transport.Transport; +import org.apache.activemq.transport.TransportFactory; +import org.apache.activemq.transport.tcp.SslTransportFactory; +import org.apache.activemq.util.JMSExceptionSupport; + +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.jms.JMSException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * An ActiveMQConnectionFactory that allows access to the key and trust managers used for SslConnections. + * + * There is no reason to use this class unless SSL is being used AND the key and trust managers need to be specified + * from within code. In fact, if the URI passed to this class does not have an "ssl" scheme, this class will + * pass all work on to its superclass. + * + * @author sepandm@gmail.com + * + */ +public class ActiveMQSslConnectionFactory extends ActiveMQConnectionFactory { + // The key and trust managers used to initialize the used SSLContext. + protected KeyManager[] keyManager = null; + protected TrustManager[] trustManager = null; + protected SecureRandom secureRandom = null; + + /** + * Sets the key and trust managers used when creating SSL connections. + * + * @param km The KeyManagers used. + * @param tm The TrustManagers used. + * @param random The SecureRandom number used. + */ + public void setKeyAndTrustManagers(KeyManager[] km, TrustManager[] tm, SecureRandom random) { + keyManager = km; + trustManager = tm; + secureRandom = random; + } + + /** + * Overriding to make special considerations for SSL connections. + * + * If we are not using SSL, the superclass's method is called. + * If we are using SSL, an SslConnectionFactory is used and it is given the + * needed key and trust managers. + * + * @author sepandm@gmail.com + */ + protected Transport createTransport() throws JMSException { + // If the given URI is non-ssl, let superclass handle it. + if (!brokerURL.getScheme().equals("ssl")) { + return super.createTransport(); + } + + try { + SslTransportFactory sslFactory = new SslTransportFactory(); + sslFactory.setKeyAndTrustManagers(keyManager, trustManager, secureRandom); + return sslFactory.doConnect(brokerURL); + } catch (Exception e) { + throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e); + } + } +} diff --git a/activemq-core/src/main/java/org/apache/activemq/broker/SslBrokerService.java b/activemq-core/src/main/java/org/apache/activemq/broker/SslBrokerService.java new file mode 100644 index 0000000000..080d7624b6 --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/broker/SslBrokerService.java @@ -0,0 +1,119 @@ +/** + * + * 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.broker; + +import org.apache.activemq.transport.TransportFactory; +import org.apache.activemq.transport.TransportServer; +import org.apache.activemq.transport.tcp.SslTransportFactory; + +import java.io.IOException; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.SecureRandom; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; + +/** + * A BrokerService that allows access to the key and trust managers used by SSL connections. + * + * There is no reason to use this class unless SSL is being used AND the key and trust managers need to be specified + * from within code. In fact, if the URI passed to this class does not have an "ssl" scheme, this class will + * pass all work on to its superclass. + * + * @author sepandm@gmail.com (Sepand) + */ +public class SslBrokerService extends BrokerService { + /** + * Adds a new transport connector for the given bind address. + * + * If the transport created uses SSL, it will also use the key and trust + * managers provided. Otherwise, this is the same as calling + * addConnector. + * + * @param bindAddress The address to bind to. + * @param km The KeyManager to be used. + * @param tm The trustmanager to be used. + * @param random The source of randomness for the generator. + * @return the newly connected and added transport connector. + * @throws Exception + */ + + public TransportConnector addSslConnector( + String bindAddress, + KeyManager[] km, + TrustManager[] tm, + SecureRandom random) throws Exception { + return addSslConnector( new URI(bindAddress), km, tm, random ); + } + + /** + * Adds a new transport connector for the given bind address. + * + * If the transport created uses SSL, it will also use the key and trust + * managers provided. Otherwise, this is the same as calling + * addConnector. + * + * @param bindAddress The URI to bind to. + * @param km The KeyManager to be used. + * @param tm The trustmanager to be used. + * @param random The source of randomness for the generator. + * @return the newly created and added transport connector. + * @throws Exception + */ + public TransportConnector addSslConnector( + URI bindAddress, + KeyManager[] km, + TrustManager[] tm, + SecureRandom random) throws Exception { + return addConnector(createSslTransportServer(bindAddress, km, tm, random)); + } + + /** + * Creates a TransportServer that uses the given key and trust managers. + * + * The last three parameters will be eventually passed to SSLContext.init. + * + * @param brokerURI The URI to bind to. + * @param km The KeyManager to be used. + * @param tm The trustmanager to be used. + * @param random The source of randomness for the generator. + * @return A new TransportServer that uses the given managers. + * @throws IOException If cannot handle URI. + * @throws KeyManagementException Passed on from SSL. + */ + protected TransportServer createSslTransportServer( + URI brokerURI, + KeyManager[] km, + TrustManager[] tm, + SecureRandom random) throws IOException, KeyManagementException { + + if (brokerURI.getScheme().equals("ssl")) { + // If given an SSL URI, use an SSL TransportFactory and configure + // it to use the given key and trust managers. + SslTransportFactory transportFactory = new SslTransportFactory(); + transportFactory.setKeyAndTrustManagers(km, tm, random); + + return transportFactory.doBind(getBrokerName(),brokerURI); + } else { + // Else, business as usual. + return TransportFactory.bind(getBrokerName(), brokerURI); + } + } +} diff --git a/activemq-core/src/main/java/org/apache/activemq/security/JaasAuthenticationPlugin.java.orig b/activemq-core/src/main/java/org/apache/activemq/security/JaasAuthenticationPlugin.java.orig new file mode 100644 index 0000000000..106700b4aa --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/security/JaasAuthenticationPlugin.java.orig @@ -0,0 +1,88 @@ +/** + * + * 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.security; + +import org.apache.activemq.broker.Broker; +import org.apache.activemq.broker.BrokerPlugin; + +import java.net.URL; + +/** + * Adds a JAAS based authentication security plugin + * + * @org.apache.xbean.XBean description="Provides a JAAS based authentication plugin" + * + * @version $Revision: 426366 $ + */ +public class JaasAuthenticationPlugin implements BrokerPlugin { + + private String configuration = "activemq-domain"; + private boolean discoverLoginConfig = true; + + public Broker installPlugin(Broker broker) { + initialiseJaas(); + return new JaasAuthenticationBroker(broker, configuration); + } + + + // Properties + // ------------------------------------------------------------------------- + public String getConfiguration() { + return configuration; + } + + /** + * Sets the JAAS configuration domain name used + */ + public void setConfiguration(String jaasConfiguration) { + this.configuration = jaasConfiguration; + } + + + public boolean isDiscoverLoginConfig() { + return discoverLoginConfig; + } + + /** + * Enables or disables the auto-discovery of the login.config file for JAAS to initialize itself. + * This flag is enabled by default such that if the java.security.auth.login.config system property + * is not defined then it is set to the location of the login.config file on the classpath. + */ + public void setDiscoverLoginConfig(boolean discoverLoginConfig) { + this.discoverLoginConfig = discoverLoginConfig; + } + + // Implementation methods + // ------------------------------------------------------------------------- + protected void initialiseJaas() { + if (discoverLoginConfig) { + String path = System.getProperty("java.security.auth.login.config"); + if (path == null) { + //URL resource = Thread.currentThread().getContextClassLoader().getResource("login.config"); + URL resource = null; + if (resource == null) { + resource = getClass().getClassLoader().getResource("login.config"); + } + if (resource != null) { + path = resource.getFile(); + System.setProperty("java.security.auth.login.config", path); + } + } + } + } +} diff --git a/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationBroker.java b/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationBroker.java new file mode 100644 index 0000000000..84e3e337d5 --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationBroker.java @@ -0,0 +1,120 @@ +/** + * + * 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.security; + +import org.apache.activemq.broker.Broker; +import org.apache.activemq.broker.BrokerFilter; +import org.apache.activemq.broker.ConnectionContext; +import org.apache.activemq.command.ConnectionInfo; +import org.apache.activemq.jaas.JaasCertificateCallbackHandler; +import org.apache.activemq.jaas.UserPrincipal; +import org.apache.activemq.security.JaasAuthenticationBroker.JaasSecurityContext; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Iterator; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; + +/** + * A JAAS Authentication Broker that uses SSL Certificates. + * + * This class will provide the JAAS framework with a JaasCertificateCallbackHandler that will grant JAAS access to + * incoming connections' SSL certificate chains. + * NOTE: There is a chance that the incoming connection does not have a valid certificate (has null). + * + * @author sepandm@gmail.com (Sepand) + */ +public class JaasCertificateAuthenticationBroker extends BrokerFilter { + private final String jaasConfiguration; + + /** + * Simple constructor. Leaves everything to superclass. + * + * @param next The Broker that does the actual work for this Filter. + * @param jassConfiguration The JAAS domain configuration name (refere to JAAS documentation). + */ + public JaasCertificateAuthenticationBroker(Broker next, String jaasConfiguration) { + super(next); + + this.jaasConfiguration = jaasConfiguration; + } + + /** + * Overridden to allow for authentication based on client certificates. + * + * Connections being added will be authenticated based on their certificate chain and the JAAS module specified + * through the JAAS framework. + * NOTE: The security context's username will be set to the first UserPrincipal created by the login module. + * + * @param context The context for the incoming Connection. + * @param info The ConnectionInfo Command representing the incoming connection. + */ + public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception { + + if (context.getSecurityContext() == null) { + if (!( info.getTransportContext() instanceof X509Certificate[] )) { + throw new SecurityException("Unable to authenticate transport without SSL certificate."); + } + + // Set the TCCL since it seems JAAS needs it to find the login module classes. + ClassLoader original = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(JaasAuthenticationBroker.class.getClassLoader()); + try { + // Do the login. + try { + CallbackHandler callback = + new JaasCertificateCallbackHandler((X509Certificate[])info.getTransportContext()); + LoginContext lc = new LoginContext(jaasConfiguration, callback); + lc.login(); + Subject subject = lc.getSubject(); + + String dnName = ""; + + for (Iterator iter = subject.getPrincipals().iterator(); iter.hasNext(); ) { + Principal nextPrincipal = (Principal)iter.next(); + if (nextPrincipal instanceof UserPrincipal) { + dnName = ((UserPrincipal)nextPrincipal).getName(); + break; + } + } + + SecurityContext s = new JaasSecurityContext(dnName, subject); + context.setSecurityContext(s); + } catch (Exception e) { + throw new SecurityException("User name or password is invalid.", e); + } + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } + super.addConnection(context, info); + } + + /** + * Overriding removeConnection to make sure the security context is cleaned. + */ + public void removeConnection(ConnectionContext context, ConnectionInfo info, Throwable error) throws Exception { + super.removeConnection(context, info, error); + + context.setSecurityContext(null); + } +} diff --git a/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationPlugin.java b/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationPlugin.java new file mode 100644 index 0000000000..93c66566c3 --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/security/JaasCertificateAuthenticationPlugin.java @@ -0,0 +1,36 @@ +/** + * + * 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.security; + +import org.apache.activemq.broker.Broker; + +/** + * A JAAS based SSL certificate authentication plugin. + * + * @org.apache.xbean.XBean description="Provides a JAAS based SSL certificate authentication plugin" + * + * @author sepandm@gmail.com (Sepand) + * + */ +public class JaasCertificateAuthenticationPlugin extends JaasAuthenticationPlugin { + public Broker installPlugin(Broker broker) { + initialiseJaas(); + return new JaasCertificateAuthenticationBroker(broker, configuration); + } +} diff --git a/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java b/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java new file mode 100644 index 0000000000..306a1dfc7c --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java @@ -0,0 +1,110 @@ +/** + * + * 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.transport.tcp; + +import org.apache.activemq.wireformat.WireFormat; +import org.apache.activemq.command.Command; +import org.apache.activemq.command.ConnectionInfo; +import org.apache.activemq.util.IntrospectionSupport; + +import java.io.IOException; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Map; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLPeerUnverifiedException; + +/** + * A Transport class that uses SSL and client-side certificate authentication. + * + * Client-side certificate authentication must be enabled through the constructor. + * By default, this class will have the same client authentication behavior as the socket it is passed. + * This class will set ConnectionInfo's transportContext to the SSL certificates of the client. + * NOTE: Accessor method for needClientAuth was not provided on purpose. This is because needClientAuth's value must be + * set before the socket is connected. Otherwise, unexpected situations may occur. + * + */ +class SslTransport extends TcpTransport { + /** + * Connect to a remote node such as a Broker. + * + * @param wireFormat The WireFormat to be used. + * @param socketFactory The socket factory to be used. Forcing SSLSockets + * for obvious reasons. + * @param remoteLocation The remote location. + * @param localLocation The local location. + * @param needClientAuth If set to true, the underlying socket will need + * client certificate authentication. + * @throws UnknownHostException If TcpTransport throws. + * @throws IOException If TcpTransport throws. + */ + public SslTransport(WireFormat wireFormat, SSLSocketFactory socketFactory, URI remoteLocation, URI localLocation, boolean needClientAuth) throws IOException { + super(wireFormat, socketFactory, remoteLocation, localLocation); + ((SSLSocket)this.socket).setNeedClientAuth(needClientAuth); + } + + /** + * Initialize from a ServerSocket. + * + * No access to needClientAuth is given since it is already set within the + * provided socket. + * + * @param wireFormat The WireFormat to be used. + * @param socket The Socket to be used. Forcing SSL. + * @throws IOException If TcpTransport throws. + */ + public SslTransport(WireFormat wireFormat, SSLSocket socket) throws IOException { + super(wireFormat, socket); + } + + /** + * Overriding in order to add the client's certificates to ConnectionInfo Commmands. + * + * @param command The Command coming in. + */ + public void doConsume(Command command) { + // The instanceof can be avoided, but that would require modifying the + // Command clas tree and that would require too much effort right + // now. + if ( command instanceof ConnectionInfo ) { + ConnectionInfo connectionInfo = (ConnectionInfo)command; + + SSLSocket sslSocket = (SSLSocket)this.socket; + + SSLSession sslSession = sslSocket.getSession(); + + X509Certificate[] clientCertChain; + try { + clientCertChain = + (X509Certificate[]) sslSession.getPeerCertificates(); + } catch(SSLPeerUnverifiedException e) { + clientCertChain = null; + } + + connectionInfo.setTransportContext(clientCertChain); + } + + super.doConsume(command); + } +} + diff --git a/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransportServer.java b/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransportServer.java new file mode 100644 index 0000000000..167f50f59a --- /dev/null +++ b/activemq-core/src/main/java/org/apache/activemq/transport/tcp/SslTransportServer.java @@ -0,0 +1,127 @@ +/** + * + * 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.transport.tcp; + +import org.apache.activemq.wireformat.WireFormat; +import org.apache.activemq.transport.Transport; + +import java.io.IOException; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; + +/** + * An SSL TransportServer. + * + * Allows for client certificate authentication (refer to setNeedClientAuth for + * details). + * NOTE: Client certificate authentication is disabled by default. + * + */ +public class SslTransportServer extends TcpTransportServer { + + // Specifies if sockets created from this server should needClientAuth. + private boolean needClientAuth = false; + + // Specifies if sockets created from this server should wantClientAuth. + private boolean wantClientAuth = false; + + + /** + * Constructor. + * + * @param transportFactory The factory used to create transports when connections arrive. + * @param location The location of the broker to bind to. + * @param serverSocketFactory The factory used to create this server. + * @param needClientAuth States if this server should needClientAuth. + * @throws IOException passed up from TcpTransportFactory. + * @throws URISyntaxException passed up from TcpTransportFactory. + */ + public SslTransportServer( + SslTransportFactory transportFactory, + URI location, + SSLServerSocketFactory serverSocketFactory) throws IOException, URISyntaxException { + super(transportFactory, location, serverSocketFactory); + } + + /** + * Setter for needClientAuth. + * + * When set to true, needClientAuth will set SSLSockets' needClientAuth to true forcing clients to provide + * client certificates. + */ + public void setNeedClientAuth(boolean needAuth) { + this.needClientAuth = needAuth; + } + + /** + * Getter for needClientAuth. + */ + public boolean getNeedClientAuth() { + return this.needClientAuth; + } + + /** + * Getter for wantClientAuth. + */ + public boolean getWantClientAuth() { + return this.wantClientAuth; + } + + /** + * Setter for wantClientAuth. + * + * When set to true, wantClientAuth will set SSLSockets' wantClientAuth to true forcing clients to provide + * client certificates. + */ + public void setWantClientAuth(boolean wantAuth) { + this.wantClientAuth = wantAuth; + } + + /** + * Binds this socket to the previously specified URI. + * + * Overridden to allow for proper handling of needClientAuth. + * + * @throws IOException passed up from TcpTransportServer. + */ + public void bind() throws IOException { + super.bind(); + ((SSLServerSocket)this.serverSocket).setWantClientAuth(wantClientAuth); + ((SSLServerSocket)this.serverSocket).setNeedClientAuth(needClientAuth); + } + + /** + * Used to create Transports for this server. + * + * Overridden to allow the use of SslTransports (instead of TcpTransports). + * + * @param socket The incoming socket that will be wrapped into the new Transport. + * @param format The WireFormat being used. + * @return The newly return (SSL) Transport. + * @throws IOException + */ + protected Transport createTransport(Socket socket, WireFormat format) throws IOException { + return new SslTransport(format, (SSLSocket)socket); + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/broker/StubBroker.java b/activemq-core/src/test/java/org/apache/activemq/broker/StubBroker.java new file mode 100644 index 0000000000..e48d721adb --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/broker/StubBroker.java @@ -0,0 +1,226 @@ +/** + * + * 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.broker; + +import org.apache.activemq.broker.region.Destination; +import org.apache.activemq.broker.region.Subscription; +import org.apache.activemq.command.ActiveMQDestination; +import org.apache.activemq.command.BrokerId; +import org.apache.activemq.command.BrokerInfo; +import org.apache.activemq.command.ConnectionInfo; +import org.apache.activemq.command.ConsumerInfo; +import org.apache.activemq.command.DestinationInfo; +import org.apache.activemq.command.Message; +import org.apache.activemq.command.MessageAck; +import org.apache.activemq.command.MessageDispatch; +import org.apache.activemq.command.MessageDispatchNotification; +import org.apache.activemq.command.MessagePull; +import org.apache.activemq.command.ProducerInfo; +import org.apache.activemq.command.RemoveSubscriptionInfo; +import org.apache.activemq.command.Response; +import org.apache.activemq.command.SessionInfo; +import org.apache.activemq.command.TransactionId; +import org.apache.activemq.kaha.Store; + +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +public class StubBroker implements Broker { + public LinkedList addConnectionData = new LinkedList(); + public LinkedList removeConnectionData = new LinkedList(); + + public class AddConnectionData { + public final ConnectionContext connectionContext; + public final ConnectionInfo connectionInfo; + + public AddConnectionData(ConnectionContext context, ConnectionInfo info) { + connectionContext = context; + connectionInfo = info; + } + } + + public class RemoveConnectionData { + public final ConnectionContext connectionContext; + public final ConnectionInfo connectionInfo; + public final Throwable error; + + public RemoveConnectionData(ConnectionContext context, ConnectionInfo info, Throwable error) { + connectionContext = context; + connectionInfo = info; + this.error = error; + } + } + + public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception { + addConnectionData.add(new AddConnectionData(context, info)); + } + + public void removeConnection(ConnectionContext context, ConnectionInfo info, Throwable error) throws Exception { + removeConnectionData.add(new RemoveConnectionData(context, info, error)); + } + + + // --- Blank Methods, fill in as needed --- + public void addBroker(Connection connection, BrokerInfo info) { + } + + public void addDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception { + } + + public void addProducer(ConnectionContext context, ProducerInfo info) throws Exception { + } + + public void addSession(ConnectionContext context, SessionInfo info) throws Exception { + } + + public void beginTransaction(ConnectionContext context, TransactionId xid) throws Exception { + } + + public void commitTransaction(ConnectionContext context, TransactionId xid, boolean onePhase) throws Exception { + } + + public void forgetTransaction(ConnectionContext context, TransactionId transactionId) throws Exception { + } + + public Broker getAdaptor(Class type) { + return null; + } + + public ConnectionContext getAdminConnectionContext() { + return null; + } + + public BrokerId getBrokerId() { + return null; + } + + public String getBrokerName() { + return null; + } + + public Connection[] getClients() throws Exception { + return null; + } + + public ActiveMQDestination[] getDestinations() throws Exception { + return null; + } + + public Set getDurableDestinations() { + return null; + } + + public BrokerInfo[] getPeerBrokerInfos() { + return null; + } + + public TransactionId[] getPreparedTransactions(ConnectionContext context) throws Exception { + return null; + } + + public boolean isFaultTolerantConfiguration() { + return false; + } + + public boolean isSlaveBroker() { + return false; + } + + public boolean isStopped() { + return false; + } + + public int prepareTransaction(ConnectionContext context, TransactionId xid) throws Exception { + return 0; + } + + public void processDispatch(MessageDispatch messageDispatch) { + } + + public void removeBroker(Connection connection, BrokerInfo info) { + } + + public void removeDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception { + } + + public void removeProducer(ConnectionContext context, ProducerInfo info) throws Exception { + } + + public void removeSession(ConnectionContext context, SessionInfo info) throws Exception { + } + + public void rollbackTransaction(ConnectionContext context, TransactionId xid) throws Exception { + } + + public void setAdminConnectionContext(ConnectionContext adminConnectionContext) { + } + + public void acknowledge(ConnectionContext context, MessageAck ack) throws Exception { + } + + public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { + return null; + } + + public Destination addDestination(ConnectionContext context, ActiveMQDestination destination) throws Exception { + return null; + } + + public void gc() { + } + + public Map getDestinationMap() { + return null; + } + + public Set getDestinations(ActiveMQDestination destination) { + return null; + } + + public Response messagePull(ConnectionContext context, MessagePull pull) throws Exception { + return null; + } + + public void processDispatchNotification(MessageDispatchNotification messageDispatchNotification) throws Exception { + } + + public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { + } + + public void removeDestination(ConnectionContext context, ActiveMQDestination destination, long timeout) + throws Exception { + } + + public void removeSubscription(ConnectionContext context, RemoveSubscriptionInfo info) throws Exception { + } + + public void send(ConnectionContext context, Message message) throws Exception { + } + + public void start() throws Exception { + } + + public void stop() throws Exception { + } + + public Store getTempDataStore() { + return null; + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/security/JaasCertificateAuthenticationBrokerTest.java b/activemq-core/src/test/java/org/apache/activemq/security/JaasCertificateAuthenticationBrokerTest.java new file mode 100644 index 0000000000..4c443f6b1c --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/security/JaasCertificateAuthenticationBrokerTest.java @@ -0,0 +1,206 @@ +/** + * + * 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.security; + +import junit.framework.TestCase; + +import org.apache.activemq.broker.ConnectionContext; +import org.apache.activemq.broker.StubBroker; +import org.apache.activemq.command.ConnectionInfo; +import org.apache.activemq.jaas.GroupPrincipal; +import org.apache.activemq.jaas.UserPrincipal; +import org.apache.activemq.transport.tcp.StubX509Certificate; + +import java.io.IOException; +import java.security.Principal; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; + +public class JaasCertificateAuthenticationBrokerTest extends TestCase { + StubBroker receiveBroker; + + JaasCertificateAuthenticationBroker authBroker; + + ConnectionContext connectionContext; + ConnectionInfo connectionInfo; + + protected void setUp() throws Exception { + receiveBroker = new StubBroker(); + + authBroker = new JaasCertificateAuthenticationBroker(receiveBroker, ""); + + connectionContext = new ConnectionContext(); + connectionInfo = new ConnectionInfo(); + + connectionInfo.setTransportContext(new StubX509Certificate[] {}); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void setConfiguration(Set userNames, Set groupNames, boolean loginShouldSucceed) { + HashMap configOptions = new HashMap(); + + String userNamesString; + { + Iterator iter = userNames.iterator(); + userNamesString = "" + (iter.hasNext() ? (String)iter.next() : ""); + while (iter.hasNext()) { + userNamesString += "," + (String)iter.next(); + } + } + + String groupNamesString = ""; + { + Iterator iter = groupNames.iterator(); + groupNamesString = "" + (iter.hasNext() ? (String)iter.next() : ""); + while (iter.hasNext()) { + groupNamesString += "," + (String)iter.next(); + } + } + + configOptions.put(StubLoginModule.ALLOW_LOGIN_PROPERTY, (loginShouldSucceed ? "true" : "false")); + configOptions.put(StubLoginModule.USERS_PROPERTY, userNamesString); + configOptions.put(StubLoginModule.GROUPS_PROPERTY, groupNamesString); + AppConfigurationEntry configEntry = new AppConfigurationEntry( + "org.apache.activemq.security.StubLoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + configOptions); + + StubJaasConfiguration jaasConfig = new StubJaasConfiguration(configEntry); + + Configuration.setConfiguration(jaasConfig); + } + + public void testAddConnectionSuccess() { + String dnUserName = "dnUserName"; + + HashSet userNames = new HashSet(); + userNames.add(dnUserName); + + HashSet groupNames = new HashSet(); + groupNames.add("testGroup1"); + groupNames.add("testGroup2"); + groupNames.add("tesetGroup3"); + + setConfiguration( + userNames, + groupNames, + true); + + try { + authBroker.addConnection(connectionContext, connectionInfo); + } catch (Exception e) { + fail("Call to addConnection failed: " + e.getMessage()); + } + + assertEquals("Number of addConnection calls to underlying Broker must match number of calls made to " + + "AuthenticationBroker.", + 1, receiveBroker.addConnectionData.size()); + + ConnectionContext receivedContext = + ((StubBroker.AddConnectionData)receiveBroker.addConnectionData.getFirst()).connectionContext; + + assertEquals("The SecurityContext's userName must be set to that of the UserPrincipal.", + dnUserName, receivedContext.getSecurityContext().getUserName()); + + Set receivedPrincipals = + receivedContext.getSecurityContext().getPrincipals(); + + for (Iterator iter = receivedPrincipals.iterator(); iter.hasNext(); ) { + Principal currentPrincipal = (Principal)iter.next(); + + if (currentPrincipal instanceof UserPrincipal) { + if (userNames.remove(currentPrincipal.getName())) { + // Nothing, we did good. + } else { + // Found an unknown userName. + fail("Unknown UserPrincipal found"); + } + } else if (currentPrincipal instanceof GroupPrincipal) { + if (groupNames.remove(currentPrincipal.getName())) { + // Nothing, we did good. + } else { + fail("Unknown GroupPrincipal found."); + } + } else { + fail("Unexpected Principal subclass found."); + } + } + + if (!userNames.isEmpty()) { + fail("Some usernames were not added as UserPrincipals"); + } + + if (!groupNames.isEmpty()) { + fail("Some group names were not added as GroupPrincipals"); + } + } + + public void testAddConnectionFailure() { + HashSet userNames = new HashSet(); + + HashSet groupNames = new HashSet(); + groupNames.add("testGroup1"); + groupNames.add("testGroup2"); + groupNames.add("tesetGroup3"); + + setConfiguration( + userNames, + groupNames, + false); + + boolean connectFailed = false; + try { + authBroker.addConnection(connectionContext, connectionInfo); + } catch (SecurityException e) { + connectFailed = true; + } catch (Exception e) { + fail("Failed to connect for unexpected reason: " + e.getMessage()); + } + + if (!connectFailed) { + fail("Unauthenticated connection allowed."); + } + + assertEquals("Unauthenticated connection allowed.", + true, receiveBroker.addConnectionData.isEmpty()); + } + + public void testRemoveConnection() throws Exception { + connectionContext.setSecurityContext(new StubSecurityContext()); + + authBroker.removeConnection(connectionContext, connectionInfo, new Throwable()); + + assertEquals("removeConnection should clear ConnectionContext.", + null, connectionContext.getSecurityContext()); + + assertEquals("Incorrect number of calls to underlying broker were made.", + 1, receiveBroker.removeConnectionData.size()); + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/security/StubDoNothingCallbackHandler.java b/activemq-core/src/test/java/org/apache/activemq/security/StubDoNothingCallbackHandler.java new file mode 100644 index 0000000000..d56013e946 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/security/StubDoNothingCallbackHandler.java @@ -0,0 +1,32 @@ +/** + * + * 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.security; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class StubDoNothingCallbackHandler implements CallbackHandler { + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + } + +} diff --git a/activemq-core/src/test/java/org/apache/activemq/security/StubJaasConfiguration.java b/activemq-core/src/test/java/org/apache/activemq/security/StubJaasConfiguration.java new file mode 100644 index 0000000000..18c3221e6d --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/security/StubJaasConfiguration.java @@ -0,0 +1,38 @@ +/** + * + * 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.security; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +public class StubJaasConfiguration extends Configuration { + private AppConfigurationEntry configEntry; + + public StubJaasConfiguration(AppConfigurationEntry configEntry) { + this.configEntry = configEntry; + } + + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { configEntry }; + } + + public void refresh() { + } + +} diff --git a/activemq-core/src/test/java/org/apache/activemq/security/StubLoginModule.java b/activemq-core/src/test/java/org/apache/activemq/security/StubLoginModule.java new file mode 100644 index 0000000000..525ce99d88 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/security/StubLoginModule.java @@ -0,0 +1,93 @@ +/** + * + * 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.security; + +import org.apache.activemq.jaas.GroupPrincipal; +import org.apache.activemq.jaas.UserPrincipal; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +public class StubLoginModule implements LoginModule { + public static final String ALLOW_LOGIN_PROPERTY = "org.apache.activemq.jaas.stubproperties.allow_login"; + public static final String USERS_PROPERTY = "org.apache.activemq.jaas.stubproperties.users"; + public static final String GROUPS_PROPERTY = "org.apache.activemq.jaas.stubproperties.groups"; + + private Subject subject = null; + + private String userNames[] = null; + private String groupNames[] = null; + private boolean allowLogin = false; + + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + String allowLoginString = (String)(options.get(ALLOW_LOGIN_PROPERTY)); + String usersString = (String)(options.get(USERS_PROPERTY)); + String groupsString = (String)(options.get(GROUPS_PROPERTY)); + + this.subject = subject; + + allowLogin = Boolean.parseBoolean(allowLoginString); + userNames = usersString.split(","); + groupNames = groupsString.split(","); + } + + public boolean login() throws LoginException { + if (!allowLogin) { + throw new FailedLoginException("Login was not allowed (as specified in configuration)."); + } + + return true; + } + + public boolean commit() throws LoginException { + if (!allowLogin) { + throw new FailedLoginException("Login was not allowed (as specified in configuration)."); + } + + for (int i = 0; i < userNames.length; ++i) { + if (userNames[i].length() > 0 ) { + subject.getPrincipals().add(new UserPrincipal(userNames[i])); + } + } + + for (int i = 0; i < groupNames.length; ++i) { + if (groupNames[i].length() > 0) { + subject.getPrincipals().add(new GroupPrincipal(groupNames[i])); + } + } + + return true; + } + + public boolean abort() throws LoginException { + return true; + } + + public boolean logout() throws LoginException { + subject.getPrincipals().clear(); + + return true; + } + +} diff --git a/activemq-core/src/test/java/org/apache/activemq/security/StubSecurityContext.java b/activemq-core/src/test/java/org/apache/activemq/security/StubSecurityContext.java new file mode 100644 index 0000000000..3d7f7453fb --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/security/StubSecurityContext.java @@ -0,0 +1,31 @@ +/** + * + * 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.security; + +import java.util.Set; + +public class StubSecurityContext extends SecurityContext { + StubSecurityContext() { + super(""); + } + + public Set getPrincipals() { + return null; + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportFactoryTest.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportFactoryTest.java new file mode 100644 index 0000000000..7e1080b308 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportFactoryTest.java @@ -0,0 +1,129 @@ +/** + * + * 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.transport.tcp; + +import junit.framework.TestCase; + +import org.apache.activemq.openwire.OpenWireFormat; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +public class SslTransportFactoryTest extends TestCase { + private SslTransportFactory factory; + + protected void setUp() throws Exception { + factory = new SslTransportFactory(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testBindServerOptions() throws IOException { + + SslTransportServer sslTransportServer = null; + + for (int i = 0; i < 4; ++i) { + final boolean wantClientAuth = ((i & 0x1) == 1); + final boolean needClientAuth = ((i & 0x2) == 1); + + String options = "wantClientAuth=" + (wantClientAuth ? "true" : "false") + + "&needClientAuth=" + (needClientAuth ? "true" : "false"); + + try { + sslTransportServer = (SslTransportServer) + factory.doBind("brokerId", new URI("ssl://localhost:61616?" + options)); + } catch (Exception e) { + fail("Unable to bind to address: " + e.getMessage()); + } + + assertEquals("Created ServerSocket did not have correct wantClientAuth status.", + sslTransportServer.getWantClientAuth(), wantClientAuth); + + assertEquals("Created ServerSocket did not have correct needClientAuth status.", + sslTransportServer.getNeedClientAuth(), needClientAuth); + + try { + sslTransportServer.stop(); + } catch (Exception e) { + fail("Unable to stop TransportServer: " + e.getMessage()); + } + } + } + + private int getMthNaryDigit(int number, int digitIdx, int numBase) { + return (number / ((int)Math.pow(numBase, digitIdx)) ) % numBase; + } + + public void testCompositeConfigure() throws IOException { + // The 5 options being tested. + int optionSettings[] = new int[5]; + + String optionNames[] = { + "wantClientAuth", + "needClientAuth", + "socket.wantClientAuth", + "socket.needClientAuth", + "socket.useClientMode" + }; + + // Using a trinary interpretation of i to set all possible values of stub options for socket and transport. + // 2 transport options, 3 socket options, 3 settings for each option => 3^5 = 243 combos. + for (int i = 0; i < 243; ++i) { + Map options = new HashMap(); + + for (int j = 0; j < 5; ++j) { + // -1 since the option range is [-1,1], not [0,2]. + optionSettings[j] = getMthNaryDigit(i, j, 3) - 1; + + if ( optionSettings[j] != -1) { + options.put(optionNames[j], (optionSettings[j] == 1 ? "true" : "false")); + } + } + + StubSSLSocket socketStub = new StubSSLSocket(null); + StubSslTransport transport = null; + + try { + transport = new StubSslTransport(null, socketStub); + } catch (Exception e) { + fail("Unable to create StubSslTransport: " + e.getMessage()); + } + + factory.compositeConfigure(transport, new OpenWireFormat(), options); + + if (socketStub.getWantClientAuthStatus() != optionSettings[2]) + System.out.println("sheiite"); + + assertEquals("wantClientAuth was not properly set", + optionSettings[0], transport.getWantClientAuthStatus()); + assertEquals("needClientAuth was not properly set", + optionSettings[1], transport.getNeedClientAuthStatus()); + assertEquals("socket.wantClientAuth was not properly set", + optionSettings[2], socketStub.getWantClientAuthStatus()); + assertEquals("socket.needClientAuth was not properly set", + optionSettings[3], socketStub.getNeedClientAuthStatus()); + assertEquals("socket.useClientMode was not properly set", + optionSettings[4], socketStub.getUseClientModeStatus()); + } + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportServerTest.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportServerTest.java new file mode 100644 index 0000000000..c1e4cafad5 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportServerTest.java @@ -0,0 +1,94 @@ +/** + * + * 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.transport.tcp; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.net.URI; + +public class SslTransportServerTest extends TestCase { + private SslTransportServer sslTransportServer = null; + private StubSSLServerSocket sslServerSocket = null; + + protected void setUp() throws Exception { + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void createAndBindTransportServer(boolean wantClientAuth, boolean needClientAuth, String options) + throws IOException { + sslServerSocket = new StubSSLServerSocket(); + + StubSSLSocketFactory socketFactory = new StubSSLSocketFactory(sslServerSocket); + + try { + sslTransportServer = + new SslTransportServer(null, new URI("ssl://localhost:61616?" + options), socketFactory); + } catch (Exception e) { + fail("Unable to create SslTransportServer."); + } + + sslTransportServer.setWantClientAuth(wantClientAuth); + sslTransportServer.setNeedClientAuth(needClientAuth); + + sslTransportServer.bind(); + } + + public void testWantAndNeedClientAuthSetters() throws IOException { + for (int i = 0; i < 4; ++i) { + final boolean wantClientAuth = ((i & 0x1) == 1); + final boolean needClientAuth = ((i & 0x2) == 1); + + final int expectedWantStatus = (wantClientAuth ? StubSSLServerSocket.TRUE : StubSSLServerSocket.FALSE ); + final int expectedNeedStatus = (needClientAuth ? StubSSLServerSocket.TRUE : StubSSLServerSocket.FALSE ); + + createAndBindTransportServer(wantClientAuth, needClientAuth, ""); + + assertEquals("Created ServerSocket did not have correct wantClientAuth status.", + sslServerSocket.getWantClientAuthStatus(), expectedWantStatus); + + assertEquals("Created ServerSocket did not have correct needClientAuth status.", + sslServerSocket.getNeedClientAuthStatus(), expectedNeedStatus); + } + } + + public void testWantAndNeedAuthReflection() throws IOException { + for (int i = 0; i < 4; ++i) { + final boolean wantClientAuth = ((i & 0x1) == 1); + final boolean needClientAuth = ((i & 0x2) == 1); + + final int expectedWantStatus = (wantClientAuth ? StubSSLServerSocket.TRUE : StubSSLServerSocket.FALSE ); + final int expectedNeedStatus = (needClientAuth ? StubSSLServerSocket.TRUE : StubSSLServerSocket.FALSE ); + + String options = "wantClientAuth=" + (wantClientAuth ? "true" : "false") + + "&needClientAuth=" + (needClientAuth ? "true" : "false"); + + createAndBindTransportServer(wantClientAuth, needClientAuth, options); + + assertEquals("Created ServerSocket did not have correct wantClientAuth status.", + sslServerSocket.getWantClientAuthStatus(), expectedWantStatus); + + assertEquals("Created ServerSocket did not have correct needClientAuth status.", + sslServerSocket.getNeedClientAuthStatus(), expectedNeedStatus); + } + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportTest.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportTest.java new file mode 100644 index 0000000000..07bee92e4e --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/SslTransportTest.java @@ -0,0 +1,106 @@ +/** + * + * 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.transport.tcp; + +import java.io.IOException; +import java.security.cert.X509Certificate; + +import javax.management.remote.JMXPrincipal; +import javax.net.ssl.SSLSocket; + +import junit.framework.TestCase; + +import org.apache.activemq.command.ConnectionInfo; +import org.apache.activemq.transport.StubTransportListener; +import org.apache.activemq.wireformat.ObjectStreamWireFormat; + +/** + * Unit tests for the SslTransport class. + * + */ +public class SslTransportTest extends TestCase { + + SSLSocket sslSocket; + SslTransport transport; + StubTransportListener stubListener; + + String username; + String password; + String certDistinguishedName; + + protected void setUp() throws Exception { + certDistinguishedName = "ThisNameIsDistinguished"; + username = "SomeUserName"; + password = "SomePassword"; + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void createTransportAndConsume( boolean wantAuth, boolean needAuth ) throws IOException { + JMXPrincipal principal = new JMXPrincipal( certDistinguishedName ); + X509Certificate cert = new StubX509Certificate( principal ); + StubSSLSession sslSession = + new StubSSLSession( cert ); + + sslSocket = new StubSSLSocket( sslSession ); + sslSocket.setWantClientAuth(wantAuth); + sslSocket.setNeedClientAuth(needAuth); + + SslTransport transport = new SslTransport( + new ObjectStreamWireFormat(), sslSocket ); + + stubListener = new StubTransportListener(); + + transport.setTransportListener( stubListener ); + + ConnectionInfo sentInfo = new ConnectionInfo(); + + sentInfo.setUserName(username); + sentInfo.setPassword(password); + + transport.doConsume(sentInfo); + } + + public void testKeepClientUserName() throws IOException { + createTransportAndConsume(true, true); + + final ConnectionInfo receivedInfo = + (ConnectionInfo) stubListener.getCommands().remove(); + + X509Certificate receivedCert; + + try { + receivedCert = ((X509Certificate[])receivedInfo.getTransportContext())[0]; + } catch (Exception e) { + receivedCert = null; + } + + if ( receivedCert == null ) { + fail("Transmitted certificate chain was not attached to ConnectionInfo."); + } + + assertEquals("Received certificate distinguished name did not match the one transmitted.", + certDistinguishedName, receivedCert.getSubjectDN().getName()); + + } +} + + diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLServerSocket.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLServerSocket.java new file mode 100644 index 0000000000..681d72886b --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLServerSocket.java @@ -0,0 +1,98 @@ +/** + * + * 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.transport.tcp; + +import java.io.IOException; + +import javax.net.ssl.SSLServerSocket; + +public class StubSSLServerSocket extends SSLServerSocket { + public static final int UNTOUCHED = -1; + public static final int FALSE = 0; + public static final int TRUE = 1; + + private int wantClientAuthStatus = UNTOUCHED; + private int needClientAuthStatus = UNTOUCHED; + + public StubSSLServerSocket() throws IOException { + + } + + public int getWantClientAuthStatus() { + return wantClientAuthStatus; + } + + public int getNeedClientAuthStatus() { + return needClientAuthStatus; + } + + public void setWantClientAuth(boolean want) { + wantClientAuthStatus = (want ? TRUE : FALSE); + } + + public void setNeedClientAuth(boolean need) { + needClientAuthStatus = (need ? TRUE : FALSE); + } + + // --- Stubbed methods --- + + public boolean getEnableSessionCreation() { + return false; + } + + public String[] getEnabledCipherSuites() { + return null; + } + + public String[] getEnabledProtocols() { + return null; + } + + public boolean getNeedClientAuth() { + return false; + } + + public String[] getSupportedCipherSuites() { + return null; + } + + public String[] getSupportedProtocols() { + return null; + } + + public boolean getUseClientMode() { + return false; + } + + public boolean getWantClientAuth() { + return false; + } + + public void setEnableSessionCreation(boolean flag) { + } + + public void setEnabledCipherSuites(String[] suites) { + } + + public void setEnabledProtocols(String[] protocols) { + } + + public void setUseClientMode(boolean mode) { + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSession.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSession.java new file mode 100644 index 0000000000..cefb356007 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSession.java @@ -0,0 +1,135 @@ +/** + * + * 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.transport.tcp; + +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +class StubSSLSession implements SSLSession { + + X509Certificate cert; + boolean isVerified = false; + + public StubSSLSession(X509Certificate cert) { + if ( cert != null ) { + this.isVerified = true; + this.cert = cert; + } else { + this.isVerified = false; + this.cert = null; + } + } + + public void setIsVerified( boolean verified ) { + this.isVerified = verified; + } + + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + if ( this.isVerified ) + return new X509Certificate[] { this.cert }; + else + throw new SSLPeerUnverifiedException("Socket is unverified."); + } + + + // --- Stubbed methods --- + + public byte[] getId() { + return null; + } + + public SSLSessionContext getSessionContext() { + return null; + } + + public long getCreationTime() { + return 0; + } + + public long getLastAccessedTime() { + return 0; + } + + public void invalidate() { + } + + public boolean isValid() { + return false; + } + + public void putValue(String arg0, Object arg1) { + } + + public Object getValue(String arg0) { + return null; + } + + public void removeValue(String arg0) { + } + + public String[] getValueNames() { + return null; + } + + public Certificate[] getLocalCertificates() { + return null; + } + + public javax.security.cert.X509Certificate[] getPeerCertificateChain() + throws SSLPeerUnverifiedException { + return null; + } + + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return null; + } + + public Principal getLocalPrincipal() { + return null; + } + + public String getCipherSuite() { + return null; + } + + public String getProtocol() { + return null; + } + + public String getPeerHost() { + return null; + } + + public int getPeerPort() { + return 0; + } + + public int getPacketBufferSize() { + return 0; + } + + public int getApplicationBufferSize() { + return 0; + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocket.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocket.java new file mode 100644 index 0000000000..28fbe94feb --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocket.java @@ -0,0 +1,127 @@ +/** + * + * 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.transport.tcp; + +import java.io.IOException; + +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +public class StubSSLSocket extends SSLSocket { + + public static final int UNTOUCHED = -1; + public static final int FALSE = 0; + public static final int TRUE = 1; + + private int wantClientAuthStatus = UNTOUCHED; + private int needClientAuthStatus = UNTOUCHED; + private int useClientModeStatus = UNTOUCHED; + + final StubSSLSession session; + + public StubSSLSocket(StubSSLSession ses) { + this.session = ses; + } + + public void setWantClientAuth(boolean arg0) { + this.wantClientAuthStatus = (arg0 ? TRUE : FALSE); + } + + public void setNeedClientAuth(boolean arg0) { + this.needClientAuthStatus = (arg0 ? TRUE : FALSE); + if ( session != null ) { + this.session.setIsVerified(arg0); + } + } + + public void setUseClientMode(boolean arg0) { + useClientModeStatus = (arg0 ? TRUE : FALSE); + } + + public boolean getWantClientAuth() { + return (wantClientAuthStatus == TRUE); + } + + public boolean getNeedClientAuth() { + return (needClientAuthStatus == TRUE); + } + + public boolean getUseClientMode() { + return (useClientModeStatus == TRUE ); + } + + public int getWantClientAuthStatus() { + return wantClientAuthStatus; + } + + public int getNeedClientAuthStatus() { + return needClientAuthStatus; + } + + public int getUseClientModeStatus() { + return useClientModeStatus; + } + + public SSLSession getSession() { + return this.session; + } + + + // --- Stubbed methods --- + + public String[] getSupportedCipherSuites() { + return null; + } + + public String[] getEnabledCipherSuites() { + return null; + } + + public void setEnabledCipherSuites(String[] arg0) { + } + + public String[] getSupportedProtocols() { + return null; + } + + public String[] getEnabledProtocols() { + return null; + } + + public void setEnabledProtocols(String[] arg0) { + } + + public void addHandshakeCompletedListener(HandshakeCompletedListener arg0) { + } + + public void removeHandshakeCompletedListener(HandshakeCompletedListener arg0) { + } + + public void startHandshake() throws IOException { + } + + public void setEnableSessionCreation(boolean arg0) { + } + + public boolean getEnableSessionCreation() { + return false; + } + +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocketFactory.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocketFactory.java new file mode 100644 index 0000000000..06f33161ea --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSSLSocketFactory.java @@ -0,0 +1,59 @@ +/** + * + * 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.transport.tcp; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import javax.net.ssl.SSLServerSocketFactory; + +public class StubSSLSocketFactory extends SSLServerSocketFactory { + + private final ServerSocket retServerSocket; + + public StubSSLSocketFactory(ServerSocket returnServerSocket) { + retServerSocket = returnServerSocket; + } + + public ServerSocket createServerSocket(int arg0) throws IOException { + return retServerSocket; + } + + public ServerSocket createServerSocket(int arg0, int arg1) + throws IOException { + return retServerSocket; + } + + public ServerSocket createServerSocket(int arg0, int arg1, InetAddress arg2) + throws IOException { + return retServerSocket; + } + + // --- Stubbed Methods --- + + public String[] getDefaultCipherSuites() { + return null; + } + + public String[] getSupportedCipherSuites() { + return null; + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSslTransport.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSslTransport.java new file mode 100644 index 0000000000..1d39fce7c7 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubSslTransport.java @@ -0,0 +1,52 @@ +/** + * + * 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.transport.tcp; + +import javax.net.ssl.SSLSocket; + +import org.apache.activemq.wireformat.WireFormat; + +public class StubSslTransport extends SslTransport { + public static final int UNTOUCHED = -1; + public static final int FALSE = 0; + public static final int TRUE = 1; + + private int wantClientAuthStatus = UNTOUCHED; + private int needClientAuthStatus = UNTOUCHED; + + public StubSslTransport(WireFormat wireFormat, SSLSocket socket) throws Exception { + super(wireFormat, socket); + } + + public void setWantClientAuth(boolean arg0) { + this.wantClientAuthStatus = (arg0 ? TRUE : FALSE); + } + + public void setNeedClientAuth(boolean arg0) { + this.needClientAuthStatus = (arg0 ? TRUE : FALSE); + } + + public int getWantClientAuthStatus() { + return wantClientAuthStatus; + } + + public int getNeedClientAuthStatus() { + return needClientAuthStatus; + } +} diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubX509Certificate.java b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubX509Certificate.java new file mode 100644 index 0000000000..9b027dc653 --- /dev/null +++ b/activemq-core/src/test/java/org/apache/activemq/transport/tcp/StubX509Certificate.java @@ -0,0 +1,166 @@ +/** + * + * 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.transport.tcp; + +import java.math.BigInteger; +import java.security.Principal; +import java.security.PublicKey; +import java.util.Date; +import java.util.Set; + +import java.security.cert.X509Certificate; + +public class StubX509Certificate extends X509Certificate { + public StubX509Certificate(Principal id) { + this.id = id; + } + + public Principal getSubjectDN() { + return this.id; + } + + private final Principal id; + + // --- Stubbed Methods --- + public void checkValidity() { + // TODO Auto-generated method stub + + } + + public void checkValidity(Date arg0) { + // TODO Auto-generated method stub + + } + + public int getVersion() { + // TODO Auto-generated method stub + return 0; + } + + public BigInteger getSerialNumber() { + // TODO Auto-generated method stub + return null; + } + + public Principal getIssuerDN() { + // TODO Auto-generated method stub + return null; + } + + public Date getNotBefore() { + // TODO Auto-generated method stub + return null; + } + + public Date getNotAfter() { + // TODO Auto-generated method stub + return null; + } + + public byte[] getTBSCertificate() { + // TODO Auto-generated method stub + return null; + } + + public byte[] getSignature() { + // TODO Auto-generated method stub + return null; + } + + public String getSigAlgName() { + // TODO Auto-generated method stub + return null; + } + + public String getSigAlgOID() { + // TODO Auto-generated method stub + return null; + } + + public byte[] getSigAlgParams() { + // TODO Auto-generated method stub + return null; + } + + public boolean[] getIssuerUniqueID() { + // TODO Auto-generated method stub + return null; + } + + public boolean[] getSubjectUniqueID() { + // TODO Auto-generated method stub + return null; + } + + public boolean[] getKeyUsage() { + // TODO Auto-generated method stub + return null; + } + + public int getBasicConstraints() { + // TODO Auto-generated method stub + return 0; + } + + public byte[] getEncoded() { + // TODO Auto-generated method stub + return null; + } + + public void verify(PublicKey arg0) { + // TODO Auto-generated method stub + + } + + public void verify(PublicKey arg0, String arg1) { + // TODO Auto-generated method stub + + } + + public String toString() { + // TODO Auto-generated method stub + return null; + } + + public PublicKey getPublicKey() { + // TODO Auto-generated method stub + return null; + } + + public boolean hasUnsupportedCriticalExtension() { + // TODO Auto-generated method stub + return false; + } + + public Set getCriticalExtensionOIDs() { + // TODO Auto-generated method stub + return null; + } + + public Set getNonCriticalExtensionOIDs() { + // TODO Auto-generated method stub + return null; + } + + public byte[] getExtensionValue(String arg0) { + // TODO Auto-generated method stub + return null; + } + +} \ No newline at end of file diff --git a/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateCallback.java b/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateCallback.java new file mode 100644 index 0000000000..93661eff4b --- /dev/null +++ b/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateCallback.java @@ -0,0 +1,52 @@ +/** + * + * 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.jaas; + +import javax.security.auth.callback.Callback; +import java.security.cert.X509Certificate; + +/** + * A Callback for SSL certificates. + * + * Will return a certificate chain to its client. + * + * @author sepandm@gmail.com (Sepand) + * + */ +public class CertificateCallback implements Callback { + X509Certificate certificates[] = null; + + /** + * Setter for certificate chain. + * + * @param certs The certificates to be returned. + */ + public void setCertificates(X509Certificate certs[]) { + certificates = certs; + } + + /** + * Getter for certificate chain. + * + * @return The certificates being carried. + */ + public X509Certificate[] getCertificates() { + return certificates; + } +} diff --git a/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateLoginModule.java b/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateLoginModule.java new file mode 100644 index 0000000000..c6484cecff --- /dev/null +++ b/activemq-jaas/src/main/java/org/apache/activemq/jaas/CertificateLoginModule.java @@ -0,0 +1,191 @@ +/** + * + * 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.jaas; + +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.security.cert.X509Certificate; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A LoginModule that allows for authentication based on SSL certificates. + * + * Allows for subclasses to define methods used to verify user certificates and find user groups. + * Uses CertificateCallbacks to retrieve certificates. + * + * @author sepandm@gmail.com (Sepand) + * + */ +public abstract class CertificateLoginModule implements LoginModule { + + private CallbackHandler callbackHandler; + private Subject subject; + + private X509Certificate certificates[]; + private String username = null; + private Set groups = null; + + private Set principals = new HashSet(); + + private static final Log log = LogFactory.getLog(CertificateLoginModule.class); + private boolean debug; + + /** + * Overriding to allow for proper initialization. + * + * Standard JAAS. + */ + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + + debug = "true".equalsIgnoreCase((String) options.get("debug")); + + if (debug) { + log.debug("Initialized debug"); + } + } + + /** + * Overriding to allow for certificate-based login. + * + * Standard JAAS. + */ + 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(uce.getMessage() + " Unable to obtain client certificates."); + } + certificates = ((CertificateCallback) callbacks[0]).getCertificates(); + + username = getUserNameForCertificates(certificates); + if ( username == null ) + throw new FailedLoginException("Unable to verify client certificates."); + + groups = getUserGroups(username); + + if (debug) { + log.debug("Certificate for user: " + username); + } + return true; + } + + /** + * Overriding to complete login process. + * + * Standard JAAS. + */ + public boolean commit() throws LoginException { + principals.add(new UserPrincipal(username)); + + String currentGroup = null; + for (Iterator iter = groups.iterator(); iter.hasNext(); ) { + currentGroup = (String)iter.next(); + principals.add(new GroupPrincipal(currentGroup)); + } + + subject.getPrincipals().addAll(principals); + + clear(); + + if (debug) { + log.debug("commit"); + } + return true; + } + + /** + * Standard JAAS override. + */ + public boolean abort() throws LoginException { + clear(); + + if (debug) { + log.debug("abort"); + } + return true; + } + + /** + * Standard JAAS override. + */ + public boolean logout() { + subject.getPrincipals().removeAll(principals); + principals.clear(); + + if (debug) { + log.debug("logout"); + } + return true; + } + + /** + * Helper method. + */ + private void clear() { + groups.clear(); + certificates = null; + } + + /** + * Should return a unique name corresponding to the certificates given. + * + * The name returned will be used to look up access levels as well as + * group associations. + * + * @param dn The distinguished name. + * @return The unique name if the certificate is recognized, null otherwise. + */ + protected abstract String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException; + + /** + * Should return a set of the groups this user belongs to. + * + * The groups returned will be added to the user's credentials. + * + * @param username The username of the client. This is the same name that + * getUserNameForDn returned for the user's DN. + * @return A Set of the names of the groups this user belongs to. + */ + protected abstract Set getUserGroups(final String username) throws LoginException; + +} diff --git a/activemq-jaas/src/main/java/org/apache/activemq/jaas/JaasCertificateCallbackHandler.java b/activemq-jaas/src/main/java/org/apache/activemq/jaas/JaasCertificateCallbackHandler.java new file mode 100644 index 0000000000..c4314100d7 --- /dev/null +++ b/activemq-jaas/src/main/java/org/apache/activemq/jaas/JaasCertificateCallbackHandler.java @@ -0,0 +1,71 @@ +/** + * + * 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.jaas; + + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import java.io.IOException; +import java.security.cert.X509Certificate; + +/** + * A Standard JAAS callback handler for SSL certificate requests. + * + * Will only handle callbacks of type CertificateCallback. + * + * @author sepandm@gmail.com (Sepand) + * + */ +public class JaasCertificateCallbackHandler implements CallbackHandler { + final X509Certificate certificates[]; + + /** + * Basic constructor. + * + * @param cert The certificate returned when calling back. + */ + public JaasCertificateCallbackHandler(X509Certificate certs[]) { + certificates = certs; + } + + /** + * Overriding handle method to handle certificates. + * + * @param callbacks The callbacks requested. + * @throws IOException + * @throws UnsupportedCallbackException Thrown if an unkown Callback type is encountered. + */ + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (callback instanceof CertificateCallback) { + CertificateCallback certCallback = (CertificateCallback) callback; + + certCallback.setCertificates(certificates); + + } else { + throw new UnsupportedCallbackException(callback); + } + } + } +} diff --git a/activemq-jaas/src/main/java/org/apache/activemq/jaas/JassCredentialCallback.java b/activemq-jaas/src/main/java/org/apache/activemq/jaas/JassCredentialCallback.java new file mode 100644 index 0000000000..990467350b --- /dev/null +++ b/activemq-jaas/src/main/java/org/apache/activemq/jaas/JassCredentialCallback.java @@ -0,0 +1,63 @@ +/** + * + * 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.jaas; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; + + +/** + * A JASS username password CallbackHandler. + */ +public class JassCredentialCallback implements CallbackHandler { + + private final String username; + private final String password; + + public JassCredentialCallback(String username, String password) { + this.username = username; + this.password = password; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback) callback; + if (password == null) { + passwordCallback.setPassword(null); + } + else { + passwordCallback.setPassword(password.toCharArray()); + } + } else if (callback instanceof NameCallback) { + NameCallback nameCallback = (NameCallback) callback; + if (username == null) { + nameCallback.setName(null); + } + else { + nameCallback.setName(username); + } + } + } + } +} diff --git a/activemq-jaas/src/main/java/org/apache/activemq/jaas/TextFileCertificateLoginModule.java b/activemq-jaas/src/main/java/org/apache/activemq/jaas/TextFileCertificateLoginModule.java new file mode 100644 index 0000000000..d07460dd2e --- /dev/null +++ b/activemq-jaas/src/main/java/org/apache/activemq/jaas/TextFileCertificateLoginModule.java @@ -0,0 +1,140 @@ +/** + * + * 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.jaas; + +import java.io.File; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +/** + * A LoginModule allowing for SSL certificate based authentication based on Distinguished Names (DN) stored in text + * files. + * + * The DNs are parsed using a Properties class where each line is =. + * This class also uses a group definition file where each line is =,,etc. + * The user and group files' locations must be specified in the org.apache.activemq.jaas.textfiledn.user and + * org.apache.activemq.jaas.textfiledn.user properties respectively. + * + * NOTE: This class will re-read user and group files for every authentication (i.e it does live updates of allowed + * groups and users). + * + * @author sepandm@gmail.com (Sepand) + */ +public class TextFileCertificateLoginModule extends CertificateLoginModule { + + private final String USER_FILE = "org.apache.activemq.jaas.textfiledn.user"; + private final String GROUP_FILE = "org.apache.activemq.jaas.textfiledn.group"; + + private File baseDir; + private String usersFilePathname; + private String groupsFilePathname; + + /** + * Performs initialization of file paths. + * + * A standard JAAS override. + */ + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + super.initialize(subject, callbackHandler, sharedState, options); + if (System.getProperty("java.security.auth.login.config") != null) { + baseDir = new File(System.getProperty("java.security.auth.login.config")).getParentFile(); + } else { + baseDir = new File("."); + } + + usersFilePathname = (String) options.get(USER_FILE)+""; + groupsFilePathname = (String) options.get(GROUP_FILE)+""; + } + + /** + * Overriding to allow DN authorization based on DNs specified in text files. + * + * @param certs The certificate the incoming connection provided. + * @return The user's authenticated name or null if unable to authenticate the user. + * @throws LoginException Thrown if unable to find user file or connection certificate. + */ + protected String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException { + if (certs == null) { + throw new LoginException("Client certificates not found. Cannot authenticate."); + } + + File usersFile = new File(baseDir,usersFilePathname); + + Properties users = new Properties(); + + try { + users.load(new java.io.FileInputStream(usersFile)); + } catch (IOException ioe) { + throw new LoginException("Unable to load user properties file " + usersFile); + } + + String dn = certs[0].getSubjectDN().getName(); + + for(Enumeration vals = users.elements(), keys = users.keys(); vals.hasMoreElements(); ) { + if ( ((String)vals.nextElement()).equals(dn) ) { + return (String)keys.nextElement(); + } else { + keys.nextElement(); + } + } + + return null; + } + + /** + * Overriding to allow for group discovery based on text files. + * + * @param username The name of the user being examined. This is the same name returned by + * getUserNameForCertificates. + * @return A Set of name Strings for groups this user belongs to. + * @throws LoginException Thrown if unable to find group definition file. + */ + protected Set getUserGroups(String username) throws LoginException { + File groupsFile = new File(baseDir, groupsFilePathname); + + Properties groups = new Properties(); + try { + groups.load(new java.io.FileInputStream(groupsFile)); + } catch (IOException ioe) { + throw new LoginException("Unable to load group properties file " + groupsFile); + } + Set userGroups = new HashSet(); + for (Enumeration enumeration = groups.keys(); enumeration.hasMoreElements();) { + String groupName = (String) enumeration.nextElement(); + String[] userList = (groups.getProperty(groupName) + "").split(","); + for (int i = 0; i < userList.length; i++) { + if (username.equals(userList[i])) { + userGroups.add(groupName); + break; + } + } + } + + return userGroups; + } +} diff --git a/activemq-jaas/src/test/java/org/apache/activemq/jaas/CertificateLoginModuleTest.java b/activemq-jaas/src/test/java/org/apache/activemq/jaas/CertificateLoginModuleTest.java new file mode 100644 index 0000000000..f126ebb17d --- /dev/null +++ b/activemq-jaas/src/test/java/org/apache/activemq/jaas/CertificateLoginModuleTest.java @@ -0,0 +1,144 @@ +/** + * + * 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.jaas; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Vector; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; + +public class CertificateLoginModuleTest extends TestCase { + private final String userName = "testUser"; + private final List groupNames = new Vector(); + private StubCertificateLoginModule loginModule; + + private Subject subject; + + public CertificateLoginModuleTest() { + groupNames.add("testGroup1"); + groupNames.add("testGroup2"); + groupNames.add("testGroup3"); + groupNames.add("testGroup4"); + } + + protected void setUp() throws Exception { + subject = new Subject(); + } + + protected void tearDown() throws Exception { + } + + private void loginWithCredentials(String userName, Set groupNames) throws LoginException { + loginModule = new StubCertificateLoginModule(userName, new HashSet(groupNames)); + JaasCertificateCallbackHandler callbackHandler = new JaasCertificateCallbackHandler(null); + + loginModule.initialize(subject, callbackHandler, null, new HashMap()); + + loginModule.login(); + loginModule.commit(); + } + + private void checkPrincipalsMatch(Subject subject) { + boolean nameFound = false; + boolean groupsFound[] = new boolean[groupNames.size()]; + for (int i = 0; i < groupsFound.length; ++i) { + groupsFound[i] = false; + } + + for (Iterator iter = subject.getPrincipals().iterator(); iter.hasNext(); ) { + Principal currentPrincipal = (Principal) iter.next(); + + if (currentPrincipal instanceof UserPrincipal) { + if (((UserPrincipal)currentPrincipal).getName().equals(userName)) { + if (nameFound == false) { + nameFound = true; + } else { + fail("UserPrincipal found twice."); + } + + } else { + fail("Unknown UserPrincipal found."); + } + + } else if (currentPrincipal instanceof GroupPrincipal) { + int principalIdx = groupNames.indexOf(((GroupPrincipal)currentPrincipal).getName()); + + if (principalIdx < 0) { + fail("Unknown GroupPrincipal found."); + } + + if (groupsFound[principalIdx] == false) { + groupsFound[principalIdx] = true; + } else { + fail("GroupPrincipal found twice."); + } + } else { + fail("Unknown Principal type found."); + } + } + } + + public void testLoginSuccess() throws IOException { + try { + loginWithCredentials(userName, new HashSet(groupNames)); + } catch (Exception e) { + fail("Unable to login: " + e.getMessage()); + } + + checkPrincipalsMatch(subject); + } + + public void testLoginFailure() throws IOException { + boolean loginFailed = false; + + try { + loginWithCredentials(null, new HashSet()); + } catch (LoginException e) { + loginFailed = true; + } + + if (!loginFailed) { + fail("Logged in with unknown certificate."); + } + } + + public void testLogOut() throws IOException { + try { + loginWithCredentials(userName, new HashSet(groupNames)); + } catch (Exception e) { + fail("Unable to login: " + e.getMessage()); + } + + loginModule.logout(); + + assertEquals("logout should have cleared Subject principals.", 0, subject.getPrincipals().size()); + } +} + diff --git a/activemq-jaas/src/test/java/org/apache/activemq/jaas/StubCertificateLoginModule.java b/activemq-jaas/src/test/java/org/apache/activemq/jaas/StubCertificateLoginModule.java new file mode 100644 index 0000000000..38401a6410 --- /dev/null +++ b/activemq-jaas/src/test/java/org/apache/activemq/jaas/StubCertificateLoginModule.java @@ -0,0 +1,48 @@ +/** + * + * 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.jaas; + +import java.security.cert.X509Certificate; +import java.util.Set; + +import javax.security.auth.login.LoginException; + +public class StubCertificateLoginModule extends CertificateLoginModule { + final String userName; + final Set groupNames; + + String lastUserName = null; + X509Certificate[] lastCertChain = null; + + public StubCertificateLoginModule(String userName, Set groupNames) { + this.userName = userName; + this.groupNames = groupNames; + } + + protected String getUserNameForCertificates(X509Certificate[] certs) + throws LoginException { + lastCertChain = certs; + return userName; + } + + protected Set getUserGroups(String username) throws LoginException { + lastUserName = username; + return this.groupNames; + } +}