From 3b1d20656f0e881f6db4d4faaa9edf30b4e91f8b Mon Sep 17 00:00:00 2001 From: Robert Greig Date: Mon, 29 Jan 2007 10:46:27 +0000 Subject: (Patch supplied by Tomas Restrepo) QPID-291-2.diff applied. Adds SASL capability to the .Net client. git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk@501001 13f79535-47bb-0310-9956-ffa450edef68 --- qpid/dotnet/Qpid.Client.Tests/App.config | 13 + .../Qpid.Client.Tests/Qpid.Client.Tests.csproj | 9 +- .../Security/CallbackHandlerRegistryTests.cs | 55 ++ .../Qpid.Client.Tests/connection/ConnectionTest.cs | 23 +- .../Client/AMQAuthenticationException.cs | 39 ++ qpid/dotnet/Qpid.Client/Client/AMQConnection.cs | 2 +- .../AuthenticationConfigurationSectionHandler.cs | 64 +++ .../Client/Handler/ChannelCloseMethodHandler.cs | 5 +- .../Client/Handler/ConnectionCloseMethodHandler.cs | 25 +- .../Handler/ConnectionSecureMethodHandler.cs | 27 +- .../Client/Handler/ConnectionStartMethodHandler.cs | 81 ++- .../Client/Protocol/AMQProtocolSession.cs | 8 + .../Client/Security/CallbackHandlerRegistry.cs | 114 ++++ .../Client/Security/IAMQCallbackHandler.cs | 34 ++ .../Security/UsernamePasswordCallbackHandler.cs | 56 ++ qpid/dotnet/Qpid.Client/Qpid.Client.csproj | 11 +- qpid/dotnet/Qpid.Common/Protocol/AMQConstant.cs | 97 ++++ qpid/dotnet/Qpid.Common/Qpid.Common.csproj | 3 +- qpid/dotnet/Qpid.NET.sln | 12 + qpid/dotnet/Qpid.Sasl.Tests/App.config | 12 + .../Mechanisms/AnonymousSaslClientTests.cs | 72 +++ .../Mechanisms/CramMD5SaslClientTests.cs | 90 ++++ .../Mechanisms/DigestSaslClientTests.cs | 249 +++++++++ .../Mechanisms/PlainSaslClientTests.cs | 88 ++++ .../Qpid.Sasl.Tests/Properties/AssemblyInfo.cs | 35 ++ qpid/dotnet/Qpid.Sasl.Tests/Qpid.Sasl.Tests.csproj | 66 +++ qpid/dotnet/Qpid.Sasl.Tests/SaslTests.cs | 111 ++++ qpid/dotnet/Qpid.Sasl.Tests/TestClientFactory.cs | 75 +++ qpid/dotnet/Qpid.Sasl/Callbacks.cs | 105 ++++ .../Qpid.Sasl/Configuration/SaslConfiguration.cs | 89 ++++ .../SaslConfigurationSectionHandler.cs | 83 +++ qpid/dotnet/Qpid.Sasl/DefaultClientFactory.cs | 96 ++++ qpid/dotnet/Qpid.Sasl/ISaslCallbackHandler.cs | 34 ++ qpid/dotnet/Qpid.Sasl/ISaslClient.cs | 41 ++ qpid/dotnet/Qpid.Sasl/ISaslClientFactory.cs | 39 ++ qpid/dotnet/Qpid.Sasl/MD5HMAC.cs | 115 ++++ .../Qpid.Sasl/Mechanisms/AnonymousSaslClient.cs | 69 +++ .../Qpid.Sasl/Mechanisms/CramMD5SaslClient.cs | 91 ++++ .../Qpid.Sasl/Mechanisms/DigestSaslClient.cs | 576 +++++++++++++++++++++ .../dotnet/Qpid.Sasl/Mechanisms/PlainSaslClient.cs | 81 +++ qpid/dotnet/Qpid.Sasl/Properties/AssemblyInfo.cs | 37 ++ qpid/dotnet/Qpid.Sasl/Qpid.Sasl.csproj | 63 +++ qpid/dotnet/Qpid.Sasl/Sasl.cs | 115 ++++ qpid/dotnet/Qpid.Sasl/SaslClient.cs | 145 ++++++ qpid/dotnet/Qpid.Sasl/SaslException.cs | 56 ++ qpid/dotnet/Qpid.Sasl/SaslProperties.cs | 42 ++ 46 files changed, 3307 insertions(+), 46 deletions(-) create mode 100644 qpid/dotnet/Qpid.Client.Tests/App.config create mode 100644 qpid/dotnet/Qpid.Client.Tests/Security/CallbackHandlerRegistryTests.cs create mode 100644 qpid/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs create mode 100644 qpid/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs create mode 100644 qpid/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs create mode 100644 qpid/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs create mode 100644 qpid/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs create mode 100644 qpid/dotnet/Qpid.Common/Protocol/AMQConstant.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/App.config create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/AnonymousSaslClientTests.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/CramMD5SaslClientTests.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/DigestSaslClientTests.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/PlainSaslClientTests.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Properties/AssemblyInfo.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/Qpid.Sasl.Tests.csproj create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/SaslTests.cs create mode 100644 qpid/dotnet/Qpid.Sasl.Tests/TestClientFactory.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Callbacks.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Configuration/SaslConfiguration.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Configuration/SaslConfigurationSectionHandler.cs create mode 100644 qpid/dotnet/Qpid.Sasl/DefaultClientFactory.cs create mode 100644 qpid/dotnet/Qpid.Sasl/ISaslCallbackHandler.cs create mode 100644 qpid/dotnet/Qpid.Sasl/ISaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/ISaslClientFactory.cs create mode 100644 qpid/dotnet/Qpid.Sasl/MD5HMAC.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Mechanisms/AnonymousSaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Mechanisms/CramMD5SaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Mechanisms/PlainSaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Properties/AssemblyInfo.cs create mode 100644 qpid/dotnet/Qpid.Sasl/Qpid.Sasl.csproj create mode 100644 qpid/dotnet/Qpid.Sasl/Sasl.cs create mode 100644 qpid/dotnet/Qpid.Sasl/SaslClient.cs create mode 100644 qpid/dotnet/Qpid.Sasl/SaslException.cs create mode 100644 qpid/dotnet/Qpid.Sasl/SaslProperties.cs diff --git a/qpid/dotnet/Qpid.Client.Tests/App.config b/qpid/dotnet/Qpid.Client.Tests/App.config new file mode 100644 index 0000000000..64c6def5fd --- /dev/null +++ b/qpid/dotnet/Qpid.Client.Tests/App.config @@ -0,0 +1,13 @@ + + + + +
+ + + + + + + + diff --git a/qpid/dotnet/Qpid.Client.Tests/Qpid.Client.Tests.csproj b/qpid/dotnet/Qpid.Client.Tests/Qpid.Client.Tests.csproj index b14f8405c9..5674174f69 100644 --- a/qpid/dotnet/Qpid.Client.Tests/Qpid.Client.Tests.csproj +++ b/qpid/dotnet/Qpid.Client.Tests/Qpid.Client.Tests.csproj @@ -20,6 +20,7 @@ DEBUG;TRACE prompt 4 + true pdbonly @@ -53,6 +54,7 @@ + @@ -69,6 +71,10 @@ {77064C42-24D2-4CEB-9EA2-0EF481A43205} Qpid.Common + + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75} + Qpid.Sasl + @@ -76,6 +82,7 @@ + PreserveNewest @@ -88,4 +95,4 @@ --> - \ No newline at end of file + diff --git a/qpid/dotnet/Qpid.Client.Tests/Security/CallbackHandlerRegistryTests.cs b/qpid/dotnet/Qpid.Client.Tests/Security/CallbackHandlerRegistryTests.cs new file mode 100644 index 0000000000..ec0594263f --- /dev/null +++ b/qpid/dotnet/Qpid.Client.Tests/Security/CallbackHandlerRegistryTests.cs @@ -0,0 +1,55 @@ +/* + * + * 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. + * + */ +using System; +using NUnit.Framework; +using Qpid.Client.Security; + + +namespace Qpid.Client.Tests.Security +{ + [TestFixture] + public class CallbackRegistryHandlerTests + { + [Test] + public void ParsesConfiguration() + { + CallbackHandlerRegistry registry = CallbackHandlerRegistry.Instance; + Assert.AreEqual(3, registry.Mechanisms.Length); + Assert.Contains("TEST", registry.Mechanisms); + + Type handlerType = registry.GetCallbackHandler("TEST"); + Assert.IsNotNull(handlerType); + Assert.AreEqual(typeof(TestCallbackHandler), handlerType); + } + } // class CallbackRegistryHandlerTests + + public class TestCallbackHandler : IAMQCallbackHandler + { + public void Initialize(Qpid.Client.Protocol.AMQProtocolSession session) + { + } + public void Handle(Qpid.Sasl.ISaslCallback[] callbacks) + { + } + + } // class TestCallbackHandler + +} // namespace Qpid.Client.Tests.Connection diff --git a/qpid/dotnet/Qpid.Client.Tests/connection/ConnectionTest.cs b/qpid/dotnet/Qpid.Client.Tests/connection/ConnectionTest.cs index 5aa6773e53..d5330898a0 100644 --- a/qpid/dotnet/Qpid.Client.Tests/connection/ConnectionTest.cs +++ b/qpid/dotnet/Qpid.Client.Tests/connection/ConnectionTest.cs @@ -23,13 +23,13 @@ using NUnit.Framework; using Qpid.Client.qms; using Qpid.Messaging; -namespace Qpid.Client.Tests.connection +namespace Qpid.Client.Tests.Connection { [TestFixture] public class ConnectionTest { [Test] - public void simpleConnection() + public void SimpleConnection() { ConnectionInfo connectionInfo = new QpidConnectionInfo(); connectionInfo.AddBrokerInfo(new AmqBrokerInfo("amqp", "localhost", 5672, false)); @@ -40,7 +40,7 @@ namespace Qpid.Client.Tests.connection } [Test] - public void passwordFailureConnection() + public void PasswordFailureConnection() { ConnectionInfo connectionInfo = new QpidConnectionInfo(); connectionInfo.SetPassword("rubbish"); @@ -50,16 +50,17 @@ namespace Qpid.Client.Tests.connection using (IConnection connection = new AMQConnection(connectionInfo)) { Console.WriteLine("connection = " + connection); + // wrong + Assert.Fail("Authentication succeeded but should've failed"); } } - catch (AMQException) + catch (AMQException e) { - Assert.Fail(); -// if (!(e is AMQAuthenticationException)) -// { -// Assert.Fail("Expected AMQAuthenticationException!"); -// } - } + if (!(e.InnerException is AMQAuthenticationException)) + { + Assert.Fail("Expected AMQAuthenticationException!"); + } + } } // // [Test] @@ -96,4 +97,4 @@ namespace Qpid.Client.Tests.connection // } // } } -} \ No newline at end of file +} diff --git a/qpid/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs b/qpid/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs new file mode 100644 index 0000000000..68cacad1ef --- /dev/null +++ b/qpid/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs @@ -0,0 +1,39 @@ +/* + * + * 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. + * + */ +using System; +using System.Runtime.Serialization; + +namespace Qpid.Client +{ + [Serializable] + public class AMQAuthenticationException : AMQException + { + public AMQAuthenticationException(int error, String message) + : base(error, message) + { + } + + protected AMQAuthenticationException(SerializationInfo info, StreamingContext ctxt) + : base(info, ctxt) + { + } + } +} diff --git a/qpid/dotnet/Qpid.Client/Client/AMQConnection.cs b/qpid/dotnet/Qpid.Client/Client/AMQConnection.cs index 1da46f19fd..3192b0018d 100644 --- a/qpid/dotnet/Qpid.Client/Client/AMQConnection.cs +++ b/qpid/dotnet/Qpid.Client/Client/AMQConnection.cs @@ -730,7 +730,7 @@ namespace Qpid.Client catch (AMQException e) { _lastAMQException = e; - throw e; + throw; // rethrow } } diff --git a/qpid/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs b/qpid/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs new file mode 100644 index 0000000000..54ee2c6660 --- /dev/null +++ b/qpid/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs @@ -0,0 +1,64 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Configuration; +using System.Text; + +using Qpid.Client.Security; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Client.Configuration +{ + public class AuthenticationConfigurationSectionHandler + : IConfigurationSectionHandler + { + + public object Create(object parent, object configContext, System.Xml.XmlNode section) + { + NameValueSectionHandler handler = new NameValueSectionHandler(); + IDictionary schemes = new Hashtable(); + + NameValueCollection options = (NameValueCollection) + handler.Create(parent, configContext, section); + + if ( options != null ) + { + foreach ( string key in options.Keys ) + { + Type type = Type.GetType(options[key]); + if ( type == null ) + throw new ConfigurationException(string.Format("Type '{0}' not found", key)); + if ( !typeof(IAMQCallbackHandler).IsAssignableFrom(type) ) + throw new ConfigurationException(string.Format("Type '{0}' does not implement IAMQCallbackHandler", key)); + + schemes[key] = type; + } + } + + return schemes; + } + + } // class AuthenticationConfigurationSectionHandler + +} // namespace Qpid.Client.Configuration diff --git a/qpid/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs b/qpid/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs index 1031f804a6..0ce8a393c9 100644 --- a/qpid/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs +++ b/qpid/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs @@ -21,6 +21,7 @@ using log4net; using Qpid.Client.Protocol; using Qpid.Client.State; +using Qpid.Protocol; using Qpid.Framing; namespace Qpid.Client.Handler @@ -43,9 +44,8 @@ namespace Qpid.Client.Handler AMQFrame frame = ChannelCloseOkBody.CreateAMQFrame(evt.ChannelId); evt.ProtocolSession.WriteFrame(frame); - //if (errorCode != AMQConstant.REPLY_SUCCESS.getCode()) // HACK - if (errorCode != 200) + if ( errorCode != AMQConstant.REPLY_SUCCESS.Code ) { _logger.Debug("Channel close received with errorCode " + errorCode + ", throwing exception"); evt.ProtocolSession.AMQConnection.ExceptionReceived(new AMQChannelClosedException(errorCode, "Error: " + reason)); @@ -55,3 +55,4 @@ namespace Qpid.Client.Handler } } + diff --git a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs index c3acc0b098..dea5316d25 100644 --- a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs +++ b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs @@ -19,10 +19,12 @@ * */ using System; +using System.Threading; using log4net; using Qpid.Client.Protocol; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Protocol; namespace Qpid.Client.Handler { @@ -38,16 +40,29 @@ namespace Qpid.Client.Handler int errorCode = method.ReplyCode; String reason = method.ReplyText; + // send CloseOK evt.ProtocolSession.WriteFrame(ConnectionCloseOkBody.CreateAMQFrame(evt.ChannelId)); - stateManager.ChangeState(AMQState.CONNECTION_CLOSED); - if (errorCode != 200) + + if ( errorCode != AMQConstant.REPLY_SUCCESS.Code ) { - _logger.Debug("Connection close received with error code " + errorCode); - throw new AMQConnectionClosedException(errorCode, "Error: " + reason); - } + if ( errorCode == AMQConstant.NOT_ALLOWED.Code ) + { + _logger.Info("Authentication Error: " + Thread.CurrentThread.Name); + evt.ProtocolSession.CloseProtocolSession(); + + //todo this is a bit of a fudge (could be conssidered such as each new connection needs a new state manager or at least a fresh state. + stateManager.ChangeState(AMQState.CONNECTION_NOT_STARTED); + throw new AMQAuthenticationException(errorCode, reason); + } else + { + _logger.Info("Connection close received with error code " + errorCode); + throw new AMQConnectionClosedException(errorCode, "Error: " + reason); + } + } // this actually closes the connection in the case where it is not an error. evt.ProtocolSession.CloseProtocolSession(); + stateManager.ChangeState(AMQState.CONNECTION_CLOSED); } } } diff --git a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs index 7c0fbd8f40..fe123e6745 100644 --- a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs +++ b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs @@ -21,6 +21,7 @@ using Qpid.Client.Protocol; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Sasl; namespace Qpid.Client.Handler { @@ -28,9 +29,31 @@ namespace Qpid.Client.Handler { public void MethodReceived(AMQStateManager stateManager, AMQMethodEvent evt) { - AMQFrame response = ConnectionSecureOkBody.CreateAMQFrame(evt.ChannelId, null); - evt.ProtocolSession.WriteFrame(response); + ISaslClient saslClient = evt.ProtocolSession.SaslClient; + if ( saslClient == null ) + { + throw new AMQException("No SASL client set up - cannot proceed with authentication"); + } + + + ConnectionSecureBody body = (ConnectionSecureBody)evt.Method; + + try + { + // Evaluate server challenge + byte[] response = saslClient.EvaluateChallenge(body.Challenge); + // AMQP version change: Hardwire the version to 0-8 (major=8, minor=0) + // TODO: Connect this to the session version obtained from ProtocolInitiation for this session. + // Be aware of possible changes to parameter order as versions change. + AMQFrame responseFrame = ConnectionSecureOkBody.CreateAMQFrame( + evt.ChannelId, response); + evt.ProtocolSession.WriteFrame(responseFrame); + } catch ( SaslException e ) + { + throw new AMQException("Error processing SASL challenge: " + e, e); + } } } } + diff --git a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs index e88ff3ddbd..1815bea152 100644 --- a/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs +++ b/qpid/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs @@ -19,11 +19,15 @@ * */ using System; +using System.Collections; using System.Text; using log4net; using Qpid.Client.Protocol; +using Qpid.Client.Security; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Sasl; + namespace Qpid.Client.Handler { @@ -35,36 +39,22 @@ namespace Qpid.Client.Handler { ConnectionStartBody body = (ConnectionStartBody) evt.Method; AMQProtocolSession ps = evt.ProtocolSession; - string username = ps.Username; - string password = ps.Password; try { - if (body.Mechanisms == null) + if ( body.Mechanisms == null ) { throw new AMQException("mechanism not specified in ConnectionStart method frame"); } - string allMechanisms = Encoding.ASCII.GetString(body.Mechanisms); - string[] mechanisms = allMechanisms.Split(' '); - string selectedMechanism = null; - foreach (string mechanism in mechanisms) - { - if (mechanism.Equals("PLAIN")) - { - selectedMechanism = mechanism; - break; - } - } - - if (selectedMechanism == null) + string mechanisms = Encoding.UTF8.GetString(body.Mechanisms); + string selectedMechanism = ChooseMechanism(mechanisms); + if ( selectedMechanism == null ) { throw new AMQException("No supported security mechanism found, passed: " + mechanisms); } + + byte[] saslResponse = DoAuthentication(selectedMechanism, ps); - // we always write out a null authzid which we don't currently use - byte[] plainData = new byte[1 + ps.Username.Length + 1 + ps.Password.Length]; - Encoding.UTF8.GetBytes(username, 0, username.Length, plainData, 1); - Encoding.UTF8.GetBytes(password, 0, password.Length, plainData, username.Length + 2); if (body.Locales == null) { throw new AMQException("Locales is not defined in Connection Start method"); @@ -86,8 +76,9 @@ namespace Qpid.Client.Handler clientProperties["product"] = "Qpid.NET"; clientProperties["version"] = "1.0"; clientProperties["platform"] = GetFullSystemInfo(); - AMQFrame frame = ConnectionStartOkBody.CreateAMQFrame(evt.ChannelId, clientProperties, selectedMechanism, - plainData, selectedLocale); + AMQFrame frame = ConnectionStartOkBody.CreateAMQFrame( + evt.ChannelId, clientProperties, selectedMechanism, + saslResponse, selectedLocale); ps.WriteFrame(frame); } catch (Exception e) @@ -109,5 +100,51 @@ namespace Qpid.Client.Handler // TODO: add in details here return ".NET 1.1 Client"; } + + private string ChooseMechanism(string mechanisms) + { + foreach ( string mech in mechanisms.Split(' ') ) + { + if ( CallbackHandlerRegistry.Instance.IsSupportedMechanism(mech) ) + { + return mech; + } + } + return null; + } + + private byte[] DoAuthentication(string selectedMechanism, AMQProtocolSession ps) + { + ISaslClient saslClient = Sasl.Sasl.CreateClient( + new string[] { selectedMechanism }, null, "AMQP", "localhost", + new Hashtable(), CreateCallbackHandler(selectedMechanism, ps) + ); + if ( saslClient == null ) + { + throw new AMQException("Client SASL configuration error: no SaslClient could be created for mechanism " + + selectedMechanism); + } + ps.SaslClient = saslClient; + try + { + return saslClient.HasInitialResponse ? + saslClient.EvaluateChallenge(new byte[0]) : null; + } catch ( Exception ex ) + { + ps.SaslClient = null; + throw new AMQException("Unable to create SASL client", ex); + } + } + + private IAMQCallbackHandler CreateCallbackHandler(string mechanism, AMQProtocolSession session) + { + Type type = CallbackHandlerRegistry.Instance.GetCallbackHandler(mechanism); + IAMQCallbackHandler handler = + (IAMQCallbackHandler)Activator.CreateInstance(type); + if ( handler == null ) + throw new AMQException("Unable to create callback handler: " + mechanism); + handler.Initialize(session); + return handler; + } } } diff --git a/qpid/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs b/qpid/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs index 15696f38c5..42169f31b3 100644 --- a/qpid/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs +++ b/qpid/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs @@ -24,6 +24,7 @@ using log4net; using Qpid.Client.Message; using Qpid.Client.Transport; using Qpid.Framing; +using Qpid.Sasl; namespace Qpid.Client.Protocol { @@ -104,6 +105,13 @@ namespace Qpid.Client.Protocol con.MaximumFrameSize = value.FrameMax; } } + + private ISaslClient _saslClient; + public ISaslClient SaslClient + { + get { return _saslClient; } + set { _saslClient = value; } + } /// /// Callback invoked from the BasicDeliverMethodHandler when a message has been received. diff --git a/qpid/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs b/qpid/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs new file mode 100644 index 0000000000..78f13c9f42 --- /dev/null +++ b/qpid/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs @@ -0,0 +1,114 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Configuration; +using System.Text; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +using Qpid.Client.Configuration; + +namespace Qpid.Client.Security +{ + + /// + /// Helper class to map SASL mechanisms to our + /// internal ISaslCallbackHandler implementations. + /// + /// + /// The set of configured callback handlers and their order + /// controls the selection of the SASL mechanism used for authentication. + /// + /// You can either replace the default handler for CRAM-MD5 and PLAIN + /// authentication (the two default options) using the application + /// configuration file. Configuration is done by especifying the SASL + /// mechanism name (e.g PLAIN) and the type implementing the callback handler + /// used to provide any data required by the mechanism like username and password. + /// + /// + /// Callback handler types should implement the IAMQCallbackHandler interface. + /// + /// + /// New callbacks or authentication mechanisms can be configured like this: + /// + /// + /// + /// + ///
+ /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + public sealed class CallbackHandlerRegistry + { + private static CallbackHandlerRegistry _instance = + new CallbackHandlerRegistry(); + private IDictionary _mechanism2HandlerMap; + private string[] _mechanisms; + + public static CallbackHandlerRegistry Instance + { + get { return _instance; } + } + + public string[] Mechanisms + { + get { return _mechanisms; } + } + + private CallbackHandlerRegistry() + { + _mechanism2HandlerMap = (IDictionary) + ConfigurationSettings.GetConfig("qpid.client/authentication"); + + // configure default options if not available + if ( _mechanism2HandlerMap == null ) + _mechanism2HandlerMap = new Hashtable(); + + if ( !_mechanism2HandlerMap.Contains(CramMD5SaslClient.Mechanism) ) + _mechanism2HandlerMap.Add(CramMD5SaslClient.Mechanism, typeof(UsernamePasswordCallbackHandler)); + if ( !_mechanism2HandlerMap.Contains(PlainSaslClient.Mechanism) ) + _mechanism2HandlerMap.Add(PlainSaslClient.Mechanism, typeof(UsernamePasswordCallbackHandler)); + + _mechanisms = new string[_mechanism2HandlerMap.Keys.Count]; + _mechanism2HandlerMap.Keys.CopyTo(_mechanisms, 0); + } + + public bool IsSupportedMechanism(string mechanism) + { + return _mechanism2HandlerMap.Contains(mechanism); + } + + public Type GetCallbackHandler(string mechanism) + { + return (Type)_mechanism2HandlerMap[mechanism]; + } + } +} diff --git a/qpid/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs b/qpid/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs new file mode 100644 index 0000000000..6802b90cee --- /dev/null +++ b/qpid/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs @@ -0,0 +1,34 @@ +/* + * + * 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. + * + */ +using System; +using System.Text; +using Qpid.Client.Protocol; +using Qpid.Sasl; + +namespace Qpid.Client.Security +{ + public interface IAMQCallbackHandler : ISaslCallbackHandler + { + void Initialize(AMQProtocolSession session); + } + +} + diff --git a/qpid/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs b/qpid/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs new file mode 100644 index 0000000000..a14139496c --- /dev/null +++ b/qpid/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs @@ -0,0 +1,56 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; +using Qpid.Client.Protocol; +using Qpid.Sasl; + +namespace Qpid.Client.Security +{ + internal class UsernamePasswordCallbackHandler : IAMQCallbackHandler + { + private AMQProtocolSession _session; + + public void Initialize(AMQProtocolSession session) + { + if ( session == null ) + throw new ArgumentNullException("session"); + + _session = session; + } + + public void Handle(ISaslCallback[] callbacks) + { + foreach ( ISaslCallback cb in callbacks ) + { + if ( cb is NameCallback ) + { + ((NameCallback)cb).Text = _session.Username; + } else if ( cb is PasswordCallback ) + { + ((PasswordCallback)cb).Text = _session.Password; + } + } + } + } +} diff --git a/qpid/dotnet/Qpid.Client/Qpid.Client.csproj b/qpid/dotnet/Qpid.Client/Qpid.Client.csproj index 83609b0757..19d2180a09 100644 --- a/qpid/dotnet/Qpid.Client/Qpid.Client.csproj +++ b/qpid/dotnet/Qpid.Client/Qpid.Client.csproj @@ -42,6 +42,8 @@ + + @@ -81,6 +83,9 @@ + + + @@ -126,6 +131,10 @@ {77064C42-24D2-4CEB-9EA2-0EF481A43205} Qpid.Common + + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75} + Qpid.Sasl + - \ No newline at end of file + diff --git a/qpid/dotnet/Qpid.Common/Protocol/AMQConstant.cs b/qpid/dotnet/Qpid.Common/Protocol/AMQConstant.cs new file mode 100644 index 0000000000..560ac97122 --- /dev/null +++ b/qpid/dotnet/Qpid.Common/Protocol/AMQConstant.cs @@ -0,0 +1,97 @@ +/* + * + * 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. + * + */ +using System; +using System.Collections; + +namespace Qpid.Protocol +{ + public sealed class AMQConstant + { + private int _code; + private string _name; + private static Hashtable _codeMap = new Hashtable(); + + public int Code + { + get { return _code; } + } + + public string Name + { + get { return _name; } + } + + private AMQConstant(int code, string name, bool map) + { + _code = code; + _name = name; + + if ( map ) + { + _codeMap.Add(code, this); + } + } + + public override string ToString() + { + return string.Format("{0}: {1}", Code, Name); + } + + public static AMQConstant GetConstant(int code) + { + AMQConstant c = (AMQConstant)_codeMap[code]; + if ( c == null ) + { + c = new AMQConstant(code, "unknown code", false); + } + return c; + } + + #region Constants + // + // Constants + // + public static readonly AMQConstant FRAME_MIN_SIZE = new AMQConstant(4096, "frame min size", true); + public static readonly AMQConstant FRAME_END = new AMQConstant(206, "frame end", true); + public static readonly AMQConstant REPLY_SUCCESS = new AMQConstant(200, "reply success", true); + public static readonly AMQConstant NOT_DELIVERED = new AMQConstant(310, "not delivered", true); + public static readonly AMQConstant MESSAGE_TOO_LARGE = new AMQConstant(311, "message too large", true); + public static readonly AMQConstant NO_ROUTE = new AMQConstant(312, "no route", true); + public static readonly AMQConstant NO_CONSUMERS = new AMQConstant(313, "no consumers", true); + public static readonly AMQConstant CONTEXT_IN_USE = new AMQConstant(320, "context in use", true); + public static readonly AMQConstant CONTEXT_UNKNOWN = new AMQConstant(321, "context unknown", true); + public static readonly AMQConstant INVALID_SELECTOR = new AMQConstant(322, "selector invalid", true); + public static readonly AMQConstant INVALID_PATH = new AMQConstant(402, "invalid path", true); + public static readonly AMQConstant ACCESS_REFUSED = new AMQConstant(403, "access refused", true); + public static readonly AMQConstant NOT_FOUND = new AMQConstant(404, "not found", true); + public static readonly AMQConstant FRAME_ERROR = new AMQConstant(501, "frame error", true); + public static readonly AMQConstant SYNTAX_ERROR = new AMQConstant(502, "syntax error", true); + public static readonly AMQConstant COMMAND_INVALID = new AMQConstant(503, "command invalid", true); + public static readonly AMQConstant CHANNEL_ERROR = new AMQConstant(504, "channel error", true); + public static readonly AMQConstant RESOURCE_ERROR = new AMQConstant(506, "resource error", true); + public static readonly AMQConstant NOT_ALLOWED = new AMQConstant(530, "not allowed", true); + public static readonly AMQConstant NOT_IMPLEMENTED = new AMQConstant(540, "not implemented", true); + public static readonly AMQConstant INTERNAL_ERROR = new AMQConstant(541, "internal error", true); + + #endregion // Constants + + } +} diff --git a/qpid/dotnet/Qpid.Common/Qpid.Common.csproj b/qpid/dotnet/Qpid.Common/Qpid.Common.csproj index 0e32e3ba17..e0b5d22efb 100644 --- a/qpid/dotnet/Qpid.Common/Qpid.Common.csproj +++ b/qpid/dotnet/Qpid.Common/Qpid.Common.csproj @@ -168,6 +168,7 @@ + @@ -202,4 +203,4 @@ --> - \ No newline at end of file + diff --git a/qpid/dotnet/Qpid.NET.sln b/qpid/dotnet/Qpid.NET.sln index c4b518e51c..33abed66bd 100644 --- a/qpid/dotnet/Qpid.NET.sln +++ b/qpid/dotnet/Qpid.NET.sln @@ -14,6 +14,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qpid.Common", "Qpid.Common\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qpid.Client.Tests", "Qpid.Client.Tests\Qpid.Client.Tests.csproj", "{BA1B0032-4CE6-40DD-A2DC-119F0FFA0A1D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qpid.Sasl", "Qpid.Sasl\Qpid.Sasl.csproj", "{1465B0EE-6452-42A6-AB73-B2F9EABEEE75}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qpid.Sasl.Tests", "Qpid.Sasl.Tests\Qpid.Sasl.Tests.csproj", "{587B3520-EBB9-41ED-B019-E96116B651CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,6 +52,14 @@ Global {BA1B0032-4CE6-40DD-A2DC-119F0FFA0A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA1B0032-4CE6-40DD-A2DC-119F0FFA0A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA1B0032-4CE6-40DD-A2DC-119F0FFA0A1D}.Release|Any CPU.Build.0 = Release|Any CPU + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75}.Release|Any CPU.Build.0 = Release|Any CPU + {587B3520-EBB9-41ED-B019-E96116B651CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {587B3520-EBB9-41ED-B019-E96116B651CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {587B3520-EBB9-41ED-B019-E96116B651CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {587B3520-EBB9-41ED-B019-E96116B651CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/qpid/dotnet/Qpid.Sasl.Tests/App.config b/qpid/dotnet/Qpid.Sasl.Tests/App.config new file mode 100644 index 0000000000..0e7e903e02 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/App.config @@ -0,0 +1,12 @@ + + + +
+ + + + + + + + diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/AnonymousSaslClientTests.cs b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/AnonymousSaslClientTests.cs new file mode 100644 index 0000000000..6c906d7f1b --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/AnonymousSaslClientTests.cs @@ -0,0 +1,72 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests.Mechanisms +{ + [TestFixture] + public class AnonymousSaslClientTests : ISaslCallbackHandler + { + private const string AUTHID = "nobody@nowhere.com"; + + [Test] + public void ReturnsRightMechanismName() + { + ISaslClient client = new AnonymousSaslClient(AUTHID, new Hashtable(), this); + + Assert.AreEqual("ANONYMOUS", client.MechanismName); + } + + [Test] + public void HasInitialResponseReturnsTrue() + { + ISaslClient client = new AnonymousSaslClient(AUTHID, new Hashtable(), this); + + Assert.IsTrue(client.HasInitialResponse); + } + + [Test] + public void CanEvaluateChallenge() + { + Hashtable props = new Hashtable(); + ISaslClient client = new AnonymousSaslClient(AUTHID, props, this); + + Assert.IsFalse(client.IsComplete); + byte[] response = client.EvaluateChallenge(new byte[0]); + Assert.AreEqual(AUTHID, Encoding.UTF8.GetString(response)); + + Assert.IsTrue(client.IsComplete); + } + + void ISaslCallbackHandler.Handle(ISaslCallback[] callbacks) + { + } + + } // class AnonymousSaslClientTests + +} // namespace Qpid.Sasl.Tests.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/CramMD5SaslClientTests.cs b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/CramMD5SaslClientTests.cs new file mode 100644 index 0000000000..b5c9935d10 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/CramMD5SaslClientTests.cs @@ -0,0 +1,90 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests.Mechanisms +{ + [TestFixture] + public class CramMD5SaslClientTests : ISaslCallbackHandler + { + private const string USERNAME = "testuser"; + private const string PASSWORD = "tanstaaftanstaaf"; + private const string AUTHID = "test"; + + [Test] + public void ReturnsRightMechanismName() + { + ISaslClient client = new CramMD5SaslClient(AUTHID, new Hashtable(), this); + + Assert.AreEqual("CRAM-MD5", client.MechanismName); + } + + [Test] + public void HasInitialResponseReturnsFalse() + { + ISaslClient client = new CramMD5SaslClient(AUTHID, new Hashtable(), this); + + Assert.IsFalse(client.HasInitialResponse); + } + + [Test] + public void CanEvaluateChallenge() + { + Hashtable props = new Hashtable(); + + ISaslClient client = new CramMD5SaslClient(AUTHID, props, this); + + Assert.IsFalse(client.IsComplete); + + byte[] challenge = + Encoding.UTF8.GetBytes("<1896.697170952@postoffice.reston.mci.net>"); + byte[] response = client.EvaluateChallenge(challenge); + string[] parts = Encoding.UTF8.GetString(response).Split(' '); + + Assert.AreEqual(2, parts.Length); + Assert.AreEqual(USERNAME, parts[0]); + Assert.AreEqual("b913a602c7eda7a495b4e6e7334d3890", parts[1]); + Assert.IsTrue(client.IsComplete); + } + + void ISaslCallbackHandler.Handle(ISaslCallback[] callbacks) + { + foreach ( ISaslCallback cb in callbacks ) + { + if ( cb is NameCallback ) + { + ((NameCallback)cb).Text = USERNAME; + } else if ( cb is PasswordCallback ) + { + ((PasswordCallback)cb).Text = PASSWORD; + } + } + } + } // class CramMD5SaslClientTests + +} // namespace Qpid.Sasl.Tests.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/DigestSaslClientTests.cs b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/DigestSaslClientTests.cs new file mode 100644 index 0000000000..4ed49d3806 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/DigestSaslClientTests.cs @@ -0,0 +1,249 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests.Mechanisms +{ + [TestFixture] + public class DigestSaslClientTests : ISaslCallbackHandler + { + private const string USERNAME = "chris"; + private const string PASSWORD = "secret"; + private const string AUTHID = null; + private const string PROTOCOL = "IMAP"; + private const string SERVERNAME = "elwood.innosoft.com"; + + #region Digest Challenge Parsing Tests + // + // Digest Challenge Parsing Tests + // + + [Test] + public void CanParseSimpleString() + { + string challenge = "realm=\"elwood.innosoft.com\", algorithm=md5-sess"; + StringDictionary values = DigestChallenge.ParseParameters(challenge); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("elwood.innosoft.com", values["realm"]); + Assert.AreEqual("md5-sess", values["algorithm"]); + } + + [Test] + public void CanParseEscapedQuotes() + { + string challenge = "realm=\"elwood\\\".innosoft.com\", algorithm=md5-sess"; + StringDictionary values = DigestChallenge.ParseParameters(challenge); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("elwood\\\".innosoft.com", values["realm"]); + Assert.AreEqual("md5-sess", values["algorithm"]); + } + + [Test] + public void CanParseEmbeddedDelimiter() + { + string challenge = "realm=\"elwood,innosoft.com\", algorithm=md5-sess"; + StringDictionary values = DigestChallenge.ParseParameters(challenge); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("elwood,innosoft.com", values["realm"]); + Assert.AreEqual("md5-sess", values["algorithm"]); + } + + [Test] + public void CanParse1() + { + string challenge = "realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",qop=\"auth\",algorithm=md5-sess,charset=utf-8"; + DigestChallenge parsed = DigestChallenge.Parse(challenge); + + Assert.AreEqual("elwood.innosoft.com", parsed.Realm); + Assert.AreEqual("OA6MG9tEQGm2hh", parsed.Nonce); + Assert.Contains("auth", parsed.QopOptions); + Assert.AreEqual("md5-sess", parsed.Algorithm); + Assert.AreEqual("utf-8", parsed.Charset); + } + + #endregion // Digest Challenge Parsing Tests + + + #region Digest Response Tests + // + // Digest Response Tests + // + + [Test] + public void CanWriteResponse() + { + DigestResponse resp = new DigestResponse(); + resp.Username = "user"; + resp.Realm = "nowhere.com"; + resp.Nonce = "OA9BSXrbuRhWay"; + resp.Cnonce = "OA9BSuZWMSpW8m"; + resp.NonceCount = 16; + resp.DigestUri = "acap/elwood.innosoft.com"; + resp.Response = "6084c6db3fede7352c551284490fd0fc"; + resp.Qop = "auth"; + resp.MaxBuffer = 65536; + resp.Cipher = "3des"; + resp.Authzid = "user2"; + resp.AuthParam = "ap"; + resp.Charset = "utf-8"; + + string expected = "username=\"user\",realm=\"nowhere.com\",nonce=\"OA9BSXrbuRhWay\",cnonce=\"OA9BSuZWMSpW8m\",nc=00000010,qop=auth,digest-uri=\"acap/elwood.innosoft.com\",response=\"6084c6db3fede7352c551284490fd0fc\",maxbuf=65536,charset=utf-8,cipher=3des,authzid=\"user2\",auth-param=\"ap\""; + Assert.AreEqual(expected, resp.ToString()); + } + + [Test] + public void CanWriteEscapedSecuence() + { + DigestResponse resp = new DigestResponse(); + resp.Username = "us\"er"; + + string expected = "username=\"us\\\"er\",nc=00000000,maxbuf=0"; + Assert.AreEqual(expected, resp.ToString()); + } + + #endregion // Digest Response Tests + + + #region Authentication Tests + // + // Authentication Tests + // + + [Test] + public void ReturnsRightMechanismName() + { + ISaslClient client = CreateClient(); + + Assert.AreEqual("DIGEST-MD5", client.MechanismName); + } + + [Test] + public void HasInitialResponseReturnsFalse() + { + ISaslClient client = CreateClient(); + + Assert.IsFalse(client.HasInitialResponse); + } + + [Test] + public void CanAuthenticate() + { + string challenge = "realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",qop=\"auth\",algorithm=md5-sess,charset=utf-8"; + DigestSaslClient client = CreateClient(); + client.Cnonce = "OA6MHXh6VqTrRk"; + + byte[] bresp = client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge)); + string response = Encoding.UTF8.GetString(bresp); + string expectedResp = "username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"" + + client.Cnonce + "\",nc=00000001,qop=auth,digest-uri=\"imap/elwood.innosoft.com\",response=\"d388dad90d4bbd760a152321f2143af7\",maxbuf=65536,charset=utf-8"; + + Assert.AreEqual(expectedResp, response); + Assert.IsFalse(client.IsComplete); + + string challenge2 = "rspauth=ea40f60335c427b5527b84dbabcdfffd"; + bresp = client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge2)); + // client responds with zero-length array + Assert.AreEqual(0, bresp.Length); + Assert.IsTrue(client.IsComplete); + } + + [Test] + [ExpectedException(typeof(ArgumentNullException))] + public void ThrowsExceptionWhenChallengeIsMissing() + { + DigestSaslClient client = CreateClient(); + client.EvaluateChallenge(null); + } + + + [Test] + [ExpectedException(typeof(SaslException))] + public void ThrowsExceptionWhenNonceMissing() + { + string challenge = "realm=\"elwood.innosoft.com\""; + DigestSaslClient client = CreateClient(); + + client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge)); + } + + [Test] + [ExpectedException(typeof(SaslException))] + public void ThrowsExceptionWhenAlgorithmMissing() + { + string challenge = "realm=\"elwood.innosoft.com\",nonce=\"asdasadsad\""; + DigestSaslClient client = CreateClient(); + + client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge)); + } + + [Test] + [ExpectedException(typeof(SaslException))] + public void ThrowsExceptionWhenSecondChallengeInvalid() + { + string challenge = "realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",qop=\"auth\",algorithm=md5-sess,charset=utf-8"; + DigestSaslClient client = CreateClient(); + + byte[] bresp = client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge)); + string response = Encoding.UTF8.GetString(bresp); + + // repeat challenge 1, which is incorrect + client.EvaluateChallenge(Encoding.UTF8.GetBytes(challenge)); + } + + private DigestSaslClient CreateClient() + { + return new DigestSaslClient( + AUTHID, SERVERNAME, PROTOCOL, + new Hashtable(), this + ); + } + + void ISaslCallbackHandler.Handle(ISaslCallback[] callbacks) + { + foreach ( ISaslCallback cb in callbacks ) + { + if ( cb is NameCallback ) + { + ((NameCallback)cb).Text = USERNAME; + } else if ( cb is PasswordCallback ) + { + ((PasswordCallback)cb).Text = PASSWORD; + } else if ( cb is RealmCallback ) + { + ((RealmCallback)cb).Text = SERVERNAME; + } + } + } + + #endregion // Authentication Tests + + + } // class DigestSaslClientTests + +} // namespace Qpid.Sasl.Tests.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/PlainSaslClientTests.cs b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/PlainSaslClientTests.cs new file mode 100644 index 0000000000..3f39c1980d --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Mechanisms/PlainSaslClientTests.cs @@ -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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests.Mechanisms +{ + [TestFixture] + public class PlainSaslClientTests : ISaslCallbackHandler + { + private const string USERNAME = "testuser"; + private const string PASSWORD = "thepasswd"; + private const string AUTHID = "theauth"; + + [Test] + public void ReturnsRightMechanismName() + { + ISaslClient client = new PlainSaslClient(AUTHID, new Hashtable(), this); + + Assert.AreEqual("PLAIN", client.MechanismName); + } + + [Test] + public void HasInitialResponseReturnsTrue() + { + ISaslClient client = new PlainSaslClient(AUTHID, new Hashtable(), this); + + Assert.IsTrue(client.HasInitialResponse); + } + + [Test] + public void CanEvaluateChallenge() + { + Hashtable props = new Hashtable(); + ISaslClient client = new PlainSaslClient(AUTHID, props, this); + + Assert.IsFalse(client.IsComplete); + byte[] response = client.EvaluateChallenge(new byte[0]); + string[] parts = Encoding.UTF8.GetString(response).Split('\0'); + + Assert.AreEqual(3, parts.Length); + Assert.AreEqual(AUTHID, parts[0]); + Assert.AreEqual(USERNAME, parts[1]); + Assert.AreEqual(PASSWORD, parts[2]); + Assert.IsTrue(client.IsComplete); + } + + void ISaslCallbackHandler.Handle(ISaslCallback[] callbacks) + { + foreach ( ISaslCallback cb in callbacks ) + { + if ( cb is NameCallback ) + { + ((NameCallback)cb).Text = USERNAME; + } else if ( cb is PasswordCallback ) + { + ((PasswordCallback)cb).Text = PASSWORD; + } + } + } + + } // class PlainSaslClientTests + +} // namespace Qpid.Sasl.Tests.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Properties/AssemblyInfo.cs b/qpid/dotnet/Qpid.Sasl.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6728b1d1e --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Qpid.Sasl.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Qpid.Sasl.Tests")] +[assembly: AssemblyCopyright("Copyright © 2007")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("84cc3267-8019-4fad-a426-0a40155b3352")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/qpid/dotnet/Qpid.Sasl.Tests/Qpid.Sasl.Tests.csproj b/qpid/dotnet/Qpid.Sasl.Tests/Qpid.Sasl.Tests.csproj new file mode 100644 index 0000000000..50c2cfba6f --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/Qpid.Sasl.Tests.csproj @@ -0,0 +1,66 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {587B3520-EBB9-41ED-B019-E96116B651CE} + Library + Properties + Qpid.Sasl.Tests + Qpid.Sasl.Tests + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\Qpid.Client.Tests\lib\nunit\nunit.framework.dll + + + + + + + + + + + + + + + + + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75} + Qpid.Sasl + + + + + + + + diff --git a/qpid/dotnet/Qpid.Sasl.Tests/SaslTests.cs b/qpid/dotnet/Qpid.Sasl.Tests/SaslTests.cs new file mode 100644 index 0000000000..01bb676fb6 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/SaslTests.cs @@ -0,0 +1,111 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests +{ + [TestFixture] + public class SaslTests : ISaslCallbackHandler + { + + [Test] + public void CanCreatePlain() + { + Hashtable props = new Hashtable(); + string[] mechanisms = new string[] { "PLAIN", "OTHER" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNotNull(client); + Assert.IsInstanceOfType(typeof(PlainSaslClient), client); + } + + [Test] + public void CanCreateCramMD5() + { + Hashtable props = new Hashtable(); + string[] mechanisms = new string[] { "CRAM-MD5", "OTHER" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNotNull(client); + Assert.IsInstanceOfType(typeof(CramMD5SaslClient), client); + } + + [Test] + public void CanCreateAnonymous() + { + Hashtable props = new Hashtable(); + string[] mechanisms = new string[] { "ANONYMOUS", "OTHER" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNotNull(client); + Assert.IsInstanceOfType(typeof(AnonymousSaslClient), client); + } + + [Test] + public void CanCreateDigest() + { + Hashtable props = new Hashtable(); + string[] mechanisms = new string[] { "DIGEST-MD5", "OTHER" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNotNull(client); + Assert.IsInstanceOfType(typeof(DigestSaslClient), client); + } + + [Test] + public void ReturnsNullIfNoFactoryFound() + { + Hashtable props = new Hashtable(); + props.Add(SaslProperties.PolicyNoPlainText, true); + string[] mechanisms = new string[] { "PLAIN", "OTHER" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNull(client); + } + + [Test] + public void ParsesConfigurationSection() + { + // if the TEST mechanism is available, then we know + // the configuration section worked! + Hashtable props = new Hashtable(); + string[] mechanisms = new string[] { "TEST" }; + ISaslClient client = Sasl.CreateClient(mechanisms, "", "", "", props, this); + + Assert.IsNotNull(client); + Assert.IsInstanceOfType(typeof(TestSaslClient), client); + } + + + void ISaslCallbackHandler.Handle(ISaslCallback[] callbacks) + { + } + + } // class SaslTests + +} // namespace Qpid.Sasl.Tests diff --git a/qpid/dotnet/Qpid.Sasl.Tests/TestClientFactory.cs b/qpid/dotnet/Qpid.Sasl.Tests/TestClientFactory.cs new file mode 100644 index 0000000000..f7b68adfcb --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl.Tests/TestClientFactory.cs @@ -0,0 +1,75 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using NUnit.Framework; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl.Tests +{ + public class TestClientFactory : ISaslClientFactory + { + public string[] GetSupportedMechanisms(IDictionary props) + { + return new string[] { TestSaslClient.Mechanism }; + } + + public ISaslClient CreateClient(string[] mechanisms, string authorizationId, string protocol, string serverName, IDictionary props, ISaslCallbackHandler handler) + { + foreach ( string mech in mechanisms ) + { + if ( mech == TestSaslClient.Mechanism ) + return new TestSaslClient(props, handler); + } + return null; + } + + } // class TestClientFactory + + internal class TestSaslClient : SaslClient + { + public const string Mechanism = "TEST"; + + public override string MechanismName + { + get { return Mechanism; } + } + public override bool HasInitialResponse + { + get { return false; } + } + + public TestSaslClient(IDictionary props, ISaslCallbackHandler handler) + : base("", "", "", props, handler) + { + } + + public override byte[] EvaluateChallenge(byte[] challenge) + { + throw new NotImplementedException(); + } + } // class TestSaslClient + +} // namespace Qpid.Sasl.Tests diff --git a/qpid/dotnet/Qpid.Sasl/Callbacks.cs b/qpid/dotnet/Qpid.Sasl/Callbacks.cs new file mode 100644 index 0000000000..13ed6f784e --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Callbacks.cs @@ -0,0 +1,105 @@ +/* + * + * 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. + * + */ + +using System; +using System.Text; + +namespace Qpid.Sasl +{ + /// + /// Marker interface for Sasl Callbacks + /// + public interface ISaslCallback + { + } // interface ISaslCallback + + public abstract class TextSaslCallback : ISaslCallback + { + private string _prompt; + private string _text; + private string _defaultText; + + public string Prompt + { + get { return _prompt; } + set { _prompt = value; } + } + + public string Text + { + get { + if ( _text == null || _text.Length == 0 ) + return DefaultText; + else + return _text; + } + set { _text = value; } + } + + public string DefaultText + { + get { return _defaultText; } + set { _defaultText = value; } + } + + protected TextSaslCallback(string prompt, string text, string defaultText) + { + _prompt = prompt; + _text = text; + _defaultText = defaultText; + } + + } // class TextSaslCallback + + public class NameCallback : TextSaslCallback + { + public NameCallback() + : this(Environment.UserName) + { + } + public NameCallback(string defaultText) + : base("username:", "", defaultText) + { + } + } // class NameCallback + + public class PasswordCallback : TextSaslCallback + { + public PasswordCallback() + : base("password:", "", "") + { + } + } // class PasswordCallback + + public class RealmCallback : TextSaslCallback + { + public RealmCallback() + : this("localhost") + { + } + public RealmCallback(string defaultText) + : base("realm:", "", defaultText) + { + } + } // class RealmCallback + +} // namespace Qpid.Sasl + diff --git a/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfiguration.cs b/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfiguration.cs new file mode 100644 index 0000000000..d5f428a52e --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfiguration.cs @@ -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. + * + */ + +using System; +using System.Collections; +using System.Configuration; +using System.Text; +using System.Xml; + +namespace Qpid.Sasl.Configuration +{ + /// + /// Represents an Sasl configuration section + /// in the config file + /// + internal class SaslConfiguration + { + private IList _clientFactories; + + /// + /// Set of configured client factores + /// + public IList ClientFactories + { + get { return _clientFactories; } + } + + internal SaslConfiguration(IList clientFactoryTypes) + { + _clientFactories = new ArrayList(); + foreach ( Type type in clientFactoryTypes ) + { + _clientFactories.Add(Activator.CreateInstance(type)); + } + } + + /// + /// Get the configuration for the library + /// + /// The configuration from app.config or a default configuration + internal static SaslConfiguration GetConfiguration() + { + // 'obsolete' warning, but needed for .NET 1.1 compatibility + SaslConfiguration config = (SaslConfiguration) + ConfigurationSettings.GetConfig("qpid.sasl"); + if ( config == null ) + { + // create default configuration + IList clientFactories = GetDefaultClientFactories(); + config = new SaslConfiguration(clientFactories); + } + return config; + } + + /// + /// Create a list filled with the default client + /// factories supported by the library + /// + /// The list of client factory types + internal static IList GetDefaultClientFactories() + { + IList clientFactories = new ArrayList(); + clientFactories.Add(typeof(DefaultClientFactory)); + return clientFactories; + } + + + } // class SaslConfiguration + +} // namespace Qpid.Sasl.Configuration + diff --git a/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfigurationSectionHandler.cs b/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfigurationSectionHandler.cs new file mode 100644 index 0000000000..c307c73eb1 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Configuration/SaslConfigurationSectionHandler.cs @@ -0,0 +1,83 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Configuration; +using System.Text; +using System.Xml; + +namespace Qpid.Sasl.Configuration +{ + /// + /// Defines the configuration section to configure extra + /// Sasl client factories + /// + public class SaslConfigurationSectionHandler + : IConfigurationSectionHandler + { + public object Create(object parent, object configContext, XmlNode section) + { + IList clientFactories = SaslConfiguration.GetDefaultClientFactories(); + + foreach ( XmlNode node in section.ChildNodes ) + { + if ( node.LocalName == "clientFactories" ) + { + ProcessFactories(node, clientFactories); + } + } + + SaslConfiguration config = new SaslConfiguration(clientFactories); + return config; + } + + + private void ProcessFactories(XmlNode node, IList factories) + { + foreach ( XmlNode child in node.ChildNodes ) + { + Type type; + switch ( child.LocalName ) + { + case "add": + type = Type.GetType(child.Attributes["type"].Value); + if ( !factories.Contains(type) ) + factories.Add(type); + break; + case "remove": + type = Type.GetType(child.Attributes["type"].Value); + if ( factories.Contains(type) ) + factories.Remove(type); + break; + case "clear": + factories.Clear(); + break; + default: + // gives obsolete warning but needed for .NET 1.1 support + throw new ConfigurationException(string.Format("Unknown element '{0}' in section '{0}'", child.LocalName, node.LocalName)); + } + } + } + } // class SaslConfigurationSectionHandler + +} // namespace Qpid.Sasl.Configuration + diff --git a/qpid/dotnet/Qpid.Sasl/DefaultClientFactory.cs b/qpid/dotnet/Qpid.Sasl/DefaultClientFactory.cs new file mode 100644 index 0000000000..43f0470a21 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/DefaultClientFactory.cs @@ -0,0 +1,96 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Sasl +{ + public class DefaultClientFactory : ISaslClientFactory + { + private static readonly string[] SUPPORTED = new string[] { + DigestSaslClient.Mechanism, + CramMD5SaslClient.Mechanism, + PlainSaslClient.Mechanism, + AnonymousSaslClient.Mechanism, + }; + + public string[] GetSupportedMechanisms(IDictionary props) + { + if ( props == null ) + throw new ArgumentNullException("props"); + + ArrayList vetoed = new ArrayList(); + + if ( props.Contains(SaslProperties.PolicyNoPlainText) || + props.Contains(SaslProperties.PolicyNoDictionary) || + props.Contains(SaslProperties.PolicyNoActive) || + props.Contains(SaslProperties.PolicyForwardSecrecy) || + props.Contains(SaslProperties.PolicyPassCredentials) ) + { + vetoed.Add(CramMD5SaslClient.Mechanism); + vetoed.Add(PlainSaslClient.Mechanism); + vetoed.Add(AnonymousSaslClient.Mechanism); + } + if ( props.Contains(SaslProperties.PolicyNoAnonymous) ) + { + vetoed.Add(AnonymousSaslClient.Mechanism); + } + + ArrayList available = new ArrayList(); + foreach ( string mech in SUPPORTED ) + { + if ( !vetoed.Contains(mech) ) + available.Add(mech); + } + return (string[])available.ToArray(typeof(string)); + } + + public ISaslClient CreateClient( + string[] mechanisms, string authorizationId, + string protocol, string serverName, + IDictionary props, ISaslCallbackHandler handler + ) + { + foreach ( string mech in mechanisms ) + { + switch ( mech ) + { + case PlainSaslClient.Mechanism: + return new PlainSaslClient(authorizationId, props, handler); + case CramMD5SaslClient.Mechanism: + return new CramMD5SaslClient(authorizationId, props, handler); + case AnonymousSaslClient.Mechanism: + return new AnonymousSaslClient(authorizationId, props, handler); + case DigestSaslClient.Mechanism: + return new DigestSaslClient(authorizationId, serverName, protocol, props, handler); + } + } + // unknown mechanism + return null; + } + } // class DefaultClientFactory + +} // namespace Qpid.Sasl + diff --git a/qpid/dotnet/Qpid.Sasl/ISaslCallbackHandler.cs b/qpid/dotnet/Qpid.Sasl/ISaslCallbackHandler.cs new file mode 100644 index 0000000000..ec34755f3e --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/ISaslCallbackHandler.cs @@ -0,0 +1,34 @@ +/* + * + * 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. + * + */ + +using System; +using System.Text; + +namespace Qpid.Sasl +{ + public interface ISaslCallbackHandler + { + void Handle(ISaslCallback[] callbacks); + + } // interface ISaslCallbackHandler + +} // namespace Qpid.Sasl + diff --git a/qpid/dotnet/Qpid.Sasl/ISaslClient.cs b/qpid/dotnet/Qpid.Sasl/ISaslClient.cs new file mode 100644 index 0000000000..3ecccc0fac --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/ISaslClient.cs @@ -0,0 +1,41 @@ +/* + * + * 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. + * + */ + +using System; +using System.Text; + +namespace Qpid.Sasl +{ + public interface ISaslClient + { + string MechanismName { get; } + bool HasInitialResponse { get; } + bool IsComplete { get; } + + byte[] EvaluateChallenge(byte[] challenge); + object GetNegotiatedProperty(string propName); + byte[] Unwrap(byte[] buffer, int offset, int length); + byte[] Wrap(byte[] buffer, int offset, int lenght); + + } // interface ISaslClient + +} // namespace Qpid.Sasl + diff --git a/qpid/dotnet/Qpid.Sasl/ISaslClientFactory.cs b/qpid/dotnet/Qpid.Sasl/ISaslClientFactory.cs new file mode 100644 index 0000000000..4a5d4d0da4 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/ISaslClientFactory.cs @@ -0,0 +1,39 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +namespace Qpid.Sasl +{ + public interface ISaslClientFactory + { + string[] GetSupportedMechanisms(IDictionary props); + ISaslClient CreateClient( + string[] mechanisms, string authorizationId, + string protocol, string serverName, + IDictionary props, ISaslCallbackHandler handler + ); + } // interface ISaslClientFactory + +} // namespace Qpid.Sasl + diff --git a/qpid/dotnet/Qpid.Sasl/MD5HMAC.cs b/qpid/dotnet/Qpid.Sasl/MD5HMAC.cs new file mode 100644 index 0000000000..0696487d10 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/MD5HMAC.cs @@ -0,0 +1,115 @@ +/* + * + * 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. + * + */ + +using System; +using System.Security.Cryptography; + +namespace Qpid.Sasl +{ + /// + /// Rough HMAC MD5 implementation as presented in + /// RFC 2104. Used because the HMACMD5 class in the + /// .NET framework is not available in v1.1. + /// + public sealed class MD5HMAC : IDisposable + { + private const int BLOCK_LEN = 64; + private MD5 _hash; + private byte[] _key; + private byte[] _ipad; + private byte[] _opad; + + public MD5HMAC(byte[] key) + { + if ( key == null || key.Length == 0 ) + throw new ArgumentNullException("key"); + + _hash = new MD5CryptoServiceProvider(); + + byte[] theKey = key; + if ( theKey.Length > BLOCK_LEN ) + { + theKey = _hash.ComputeHash(theKey); + } + // pad key with 0's up to BLOCK_LEN + _key = new byte[BLOCK_LEN]; + Array.Copy(theKey, _key, theKey.Length); + + CreatePads(); + } + + public byte[] ComputeHash(byte[] input) + { + // H(K XOR opad, H(K XOR ipad, text)) + return H(_opad, H(_ipad, input)); + } + + public void Dispose() + { + if ( _hash != null ) + { + ((IDisposable)_hash).Dispose(); + _hash = null; + } + } + + #region Private Methods + // + // Private Methods + // + + private void CreatePads() + { + _ipad = new byte[BLOCK_LEN]; + _opad = new byte[BLOCK_LEN]; + for ( int i = 0; i < BLOCK_LEN; i++ ) + { + _ipad[i] = 0x36; + _opad[i] = 0x5c; + } + + XOR(_ipad, _key); + XOR(_opad, _key); + } + + private static void XOR(byte[] dest, byte[] other) + { + // assume both are same size + for ( int i = 0; i < dest.Length; i++ ) + { + dest[i] ^= other[i]; + } + } + + private byte[] H(byte[] v1, byte[] v2) + { + byte[] total = new byte[v1.Length + v2.Length]; + Array.Copy(v1, total, v1.Length); + Array.Copy(v2, 0, total, v1.Length, v2.Length); + + return _hash.ComputeHash(total); + } + + #endregion // Private Methods + + } // class MD5HMAC + +} // namespace Qpid.Sasl diff --git a/qpid/dotnet/Qpid.Sasl/Mechanisms/AnonymousSaslClient.cs b/qpid/dotnet/Qpid.Sasl/Mechanisms/AnonymousSaslClient.cs new file mode 100644 index 0000000000..9adcfd9d13 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Mechanisms/AnonymousSaslClient.cs @@ -0,0 +1,69 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +namespace Qpid.Sasl.Mechanisms +{ + /// + /// Implements the ANONYMOUS authentication mechanism + /// as outlined in RFC 2245 + /// + public class AnonymousSaslClient : SaslClient + { + public const string Mechanism = "ANONYMOUS"; + + public AnonymousSaslClient( + string authid, IDictionary properties, + ISaslCallbackHandler handler) + : base(authid, null, null, properties, handler) + { + } + + #region ISaslClient Implementation + // + // ISaslClient Implementation + // + + public override string MechanismName + { + get { return Mechanism; } + } + + public override bool HasInitialResponse + { + get { return true; } + } + + public override byte[] EvaluateChallenge(byte[] challenge) + { + // ignore challenge + SetComplete(); + return Encoding.UTF8.GetBytes(AuthorizationId); + } + + #endregion // ISaslClient Implementation + + } // class AnonymousSaslClient + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/Mechanisms/CramMD5SaslClient.cs b/qpid/dotnet/Qpid.Sasl/Mechanisms/CramMD5SaslClient.cs new file mode 100644 index 0000000000..b5190daf0d --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Mechanisms/CramMD5SaslClient.cs @@ -0,0 +1,91 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Security.Cryptography; +using System.Text; + +namespace Qpid.Sasl.Mechanisms +{ + /// + /// Implements the CRAM-MD5 authentication mechanism as outlined + /// in RFC 2195 + /// + public class CramMD5SaslClient : SaslClient + { + public const string Mechanism = "CRAM-MD5"; + private const int MinPwdLen = 16; + + public CramMD5SaslClient( + string authorizationId, + IDictionary properties, + ISaslCallbackHandler handler) + : base(authorizationId, null, null, properties, handler) + { + } + + #region ISaslClient Implementation + // + // ISaslClient Implementation + // + + public override string MechanismName + { + get { return Mechanism; } + } + + public override bool HasInitialResponse + { + get { return false; } + } + + public override byte[] EvaluateChallenge(byte[] challenge) + { + if ( challenge == null || challenge.Length == 0 ) + throw new ArgumentNullException("challenge"); + + NameCallback nameCB = new NameCallback(AuthorizationId); + PasswordCallback pwdCB = new PasswordCallback(); + ISaslCallback[] callbacks = { nameCB, pwdCB }; + Handler.Handle(callbacks); + + string username = nameCB.Text; + string passwd = pwdCB.Text.PadRight(MinPwdLen, '\0'); + + byte[] secret = Encoding.UTF8.GetBytes(passwd); + + //using ( HMAC hmac = new HMACMD5(secret) ) + using ( MD5HMAC hmac = new MD5HMAC(secret) ) + { + byte[] value = hmac.ComputeHash(challenge); + string encoded = ToHex(value); + SetComplete(); + return Encoding.UTF8.GetBytes(username + " " + encoded); + } + + } + + #endregion // ISaslClient Implementation + + } // class CramMD5SaslClient + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs b/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs new file mode 100644 index 0000000000..1f4b2cebdb --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs @@ -0,0 +1,576 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace Qpid.Sasl.Mechanisms +{ + + /// + /// Implements the DIGEST MD5 authentication mechanism + /// as outlined in RFC 2831 + /// + public class DigestSaslClient : SaslClient + { + public const string Mechanism = "DIGEST-MD5"; + private static readonly MD5 _md5 = new MD5CryptoServiceProvider(); + private int _state; + private string _cnonce; + private Encoding _encoding = Encoding.UTF8; + + public string Cnonce + { + get { return _cnonce; } + set { _cnonce = value; } + } + + public DigestSaslClient( + string authid, string serverName, string protocol, + IDictionary properties, ISaslCallbackHandler handler) + : base(authid, serverName, protocol, properties, handler) + { + _cnonce = Guid.NewGuid().ToString("N"); + } + + #region ISaslClient Implementation + // + // ISaslClient Implementation + // + + public override string MechanismName + { + get { return Mechanism; } + } + + public override bool HasInitialResponse + { + get { return false; } + } + + public override byte[] EvaluateChallenge(byte[] challenge) + { + if ( challenge == null || challenge.Length <= 0 ) + throw new ArgumentNullException("challenge"); + + switch ( _state++ ) + { + case 0: return OnInitialChallenge(challenge); + case 1: return OnFinalResponse(challenge); + } + throw new SaslException("Invalid State for authentication"); + } + + #endregion // ISaslClient Implementation + + + #region Private Methods + // + // Private Methods + // + + /// + /// Process the first challenge from the server + /// and calculate a response + /// + /// The server issued challenge + /// Client response + private byte[] OnInitialChallenge(byte[] challenge) + { + DigestChallenge dch = + DigestChallenge.Parse(_encoding.GetString(challenge)); + // validate input challenge + if ( dch.Nonce == null || dch.Nonce.Length == 0 ) + throw new SaslException("Nonce value missing in server challenge"); + if ( dch.Algorithm != "md5-sess" ) + throw new SaslException("Invalid or missing algorithm value in server challenge"); + + + NameCallback nameCB = new NameCallback(AuthorizationId); + PasswordCallback pwdCB = new PasswordCallback(); + RealmCallback realmCB = new RealmCallback(dch.Realm); + ISaslCallback[] callbacks = { nameCB, pwdCB, realmCB }; + Handler.Handle(callbacks); + + DigestResponse response = new DigestResponse(); + response.Username = nameCB.Text; + response.Realm = realmCB.Text; + response.Nonce = dch.Nonce; + response.Cnonce = Cnonce; + response.NonceCount = 1; + response.Qop = DigestQop.Auth; // only auth supported for now + response.DigestUri = Protocol.ToLower() + "/" + ServerName; + response.MaxBuffer = dch.MaxBuffer; + response.Charset = dch.Charset; + response.Cipher = null; // not supported for now + response.Authzid = AuthorizationId; + response.AuthParam = dch.AuthParam; + + response.Response = CalculateResponse( + nameCB.Text, realmCB.Text, pwdCB.Text, + dch.Nonce, response.NonceCount, response.Qop, response.DigestUri + ); + + return _encoding.GetBytes(response.ToString()); + } + + /// + /// Process the second server challenge + /// + /// Server issued challenge + /// The client response + private byte[] OnFinalResponse(byte[] challenge) + { + DigestChallenge dch = + DigestChallenge.Parse(_encoding.GetString(challenge)); + + if ( dch.Rspauth == null || dch.Rspauth.Length == 0 ) + throw new SaslException("Expected 'rspauth' in server challenge not found"); + + SetComplete(); + return new byte[0]; + } + + + + /// + /// Calculate the response field of the client response + /// + /// The user name + /// The realm + /// The user's password + /// Server nonce value + /// Client nonce count (always 1) + /// Quality of Protection + /// Digest-URI + /// The value for the response field + private string CalculateResponse( + string username, string realm, string passwd, + string nonce, int nc, string qop, string digestUri + ) + { + string a1 = CalcHexA1(username, realm, passwd, nonce); + string a2 = CalcHexA2(digestUri, qop); + + string ncs = nc.ToString("x8", CultureInfo.InvariantCulture); + StringBuilder prekd = new StringBuilder(); + prekd.Append(a1).Append(':').Append(nonce).Append(':') + .Append(ncs).Append(':').Append(Cnonce) + .Append(':').Append(qop).Append(':').Append(a2); + + return ToHex(CalcH(_encoding.GetBytes(prekd.ToString()))); + } + + private string CalcHexA1( + string username, string realm, + string passwd, string nonce + ) + { + bool hasAuthId = AuthorizationId != null && AuthorizationId.Length > 0; + + string premd = username + ":" + realm + ":" + passwd; + byte[] temp1 = CalcH(_encoding.GetBytes(premd)); + + + int a1len = 16 + 1 + nonce.Length + 1 + Cnonce.Length; + if ( hasAuthId ) + a1len += 1 + AuthorizationId.Length; + + byte[] buffer = new byte[a1len]; + Array.Copy(temp1, buffer, temp1.Length); + + string p2 = ":" + nonce + ":" + Cnonce; + if ( hasAuthId ) + p2 += ":" + AuthorizationId; + + byte[] temp2 = _encoding.GetBytes(p2); + Array.Copy(temp2, 0, buffer, 16, temp2.Length); + + return ToHex(CalcH(buffer)); + } + + private string CalcHexA2(string digestUri, string qop) + { + string a2 = "AUTHENTICATE:" + digestUri; + if ( qop != DigestQop.Auth ) + a2 += ":00000000000000000000000000000000"; + return ToHex(CalcH(_encoding.GetBytes(a2))); + } + + private static byte[] CalcH(byte[] value) + { + return _md5.ComputeHash(value); + } + + #endregion // Private Methods + + + } // class DigestSaslClient + + + /// + /// Available QOP options in the DIGEST scheme + /// + public sealed class DigestQop + { + public const string Auth = "auth"; + public const string AuthInt = "auth-int"; + public const string AuthConf = "auth-conf"; + } // class DigestQop + + + /// + /// Represents and parses a digest server challenge + /// + public class DigestChallenge + { + private string _realm = "localhost"; + private string _nonce; + private string[] _qopOptions = { DigestQop.Auth }; + private bool _stale; + private int _maxBuffer = 65536; + private string _charset = "ISO 8859-1"; + private string _algorithm; + private string[] _cipherOptions; + private string _authParam; + private string _rspauth; + + #region Properties + // + // Properties + // + + public string Realm + { + get { return _realm; } + } + + public string Nonce + { + get { return _nonce; } + } + + public string[] QopOptions + { + get { return _qopOptions; } + } + + public bool Stale + { + get { return _stale; } + } + + public int MaxBuffer + { + get { return _maxBuffer; } + set { _maxBuffer = value; } + } + + public string Charset + { + get { return _charset; } + } + + public string Algorithm + { + get { return _algorithm; } + } + + public string[] CipherOptions + { + get { return _cipherOptions; } + } + + public string AuthParam + { + get { return _authParam; } + } + + public string Rspauth + { + get { return _rspauth; } + } + + #endregion // Properties + + public static DigestChallenge Parse(string challenge) + { + DigestChallenge parsed = new DigestChallenge(); + StringDictionary parts = ParseParameters(challenge); + foreach ( string optname in parts.Keys ) + { + switch ( optname ) + { + case "realm": + parsed._realm = parts[optname]; + break; + case "nonce": + parsed._nonce = parts[optname]; + break; + case "qop-options": + parsed._qopOptions = GetOptions(parts[optname]); + break; + case "cipher-opts": + parsed._cipherOptions = GetOptions(parts[optname]); + break; + case "stale": + parsed._stale = Convert.ToBoolean(parts[optname], CultureInfo.InvariantCulture); + break; + case "maxbuf": + parsed._maxBuffer = Convert.ToInt32(parts[optname], CultureInfo.InvariantCulture); + break; + case "charset": + parsed._charset = parts[optname]; + break; + case "algorithm": + parsed._algorithm = parts[optname]; + break; + case "auth-param": + parsed._authParam = parts[optname]; + break; + case "rspauth": + parsed._rspauth = parts[optname]; + break; + } + } + + return parsed; + } + + + public static StringDictionary ParseParameters(string source) + { + if ( source == null ) + throw new ArgumentNullException("source"); + + StringDictionary ret = new StringDictionary(); + + string remaining = source.Trim(); + while ( remaining.Length > 0 ) + { + int equals = remaining.IndexOf('='); + if ( equals < 0 ) + break; + + string optname = remaining.Substring(0, equals).Trim(); + remaining = remaining.Substring(equals + 1); + + string value = ParseQuoted(ref remaining); + ret[optname] = value.Trim(); + } + return ret; + } + + private static string ParseQuoted(ref string str) + { + string ns = str.TrimStart(); + + int start = 0; + bool quoted = ns[0] == '\"'; + if ( quoted ) start++; + bool inquotes = quoted; + bool escaped = false; + + int pos = start; + for ( ; pos < ns.Length; pos++ ) + { + if ( !inquotes && ns[pos] == ',' ) + break; + + // at end of quotes? + if ( quoted && !escaped && ns[pos] == '\"' ) + inquotes = false; + // is this char an escape for the next one? + escaped = inquotes && ns[pos] == '\\'; + } + // pos has end of string + string value = ns.Substring(start, pos-start).Trim(); + if ( quoted ) + { + // remove trailing quote + value = value.Substring(0, value.Length - 1); + } + str = ns.Substring(pos < ns.Length-1 ? pos+1 : pos); + return value; + } + + private static string[] GetOptions(string value) + { + return value.Split(' '); + } + + } // class DigestChallenge + + + /// + /// Represents and knows how to write a + /// digest client response + /// + public class DigestResponse + { + private string _username; + private string _realm; + private string _nonce; + private string _cnonce; + private int _nonceCount; + private string _qop; + private string _digestUri; + private string _response; + private int _maxBuffer; + private string _charset; + private string _cipher; + private string _authzid; + private string _authParam; + + #region Properties + // + // Properties + // + + public string Username + { + get { return _username; } + set { _username = value; } + } + + public string Realm + { + get { return _realm; } + set { _realm = value; } + } + + public string Nonce + { + get { return _nonce; } + set { _nonce = value; } + } + + public string Cnonce + { + get { return _cnonce; } + set { _cnonce = value; } + } + + public int NonceCount + { + get { return _nonceCount; } + set { _nonceCount = value; } + } + + public string Qop + { + get { return _qop; } + set { _qop = value; } + } + + public string DigestUri + { + get { return _digestUri; } + set { _digestUri = value; } + } + + public string Response + { + get { return _response; } + set { _response = value; } + } + + public int MaxBuffer + { + get { return _maxBuffer; } + set { _maxBuffer = value; } + } + + public string Charset + { + get { return _charset; } + set { _charset = value; } + } + + public string Cipher + { + get { return _cipher; } + set { _cipher = value; } + } + + public string Authzid + { + get { return _authzid; } + set { _authzid = value; } + } + + public string AuthParam + { + get { return _authParam; } + set { _authParam = value; } + } + + #endregion // Properties + + + public override string ToString() + { + StringBuilder buffer = new StringBuilder(); + Pair(buffer, "username", Username, true); + Pair(buffer, "realm", Realm, true); + Pair(buffer, "nonce", Nonce, true); + Pair(buffer, "cnonce", Cnonce, true); + string nc = NonceCount.ToString("x8", CultureInfo.InvariantCulture); + Pair(buffer, "nc", nc, false); + Pair(buffer, "qop", Qop, false); + Pair(buffer, "digest-uri", DigestUri, true); + Pair(buffer, "response", Response, true); + string maxBuffer = MaxBuffer.ToString(CultureInfo.InvariantCulture); + Pair(buffer, "maxbuf", maxBuffer, false); + Pair(buffer, "charset", Charset, false); + Pair(buffer, "cipher", Cipher, false); + Pair(buffer, "authzid", Authzid, true); + Pair(buffer, "auth-param", AuthParam, true); + + return buffer.ToString().TrimEnd(','); + } + + private static void Pair(StringBuilder buffer, string name, string value, bool quoted) + { + if ( value != null && value.Length > 0 ) + { + buffer.Append(name); + buffer.Append('='); + if ( quoted ) + { + buffer.Append('\"'); + buffer.Append(value.Replace("\"", "\\\"")); + buffer.Append('\"'); + } else + { + buffer.Append(value); + } + buffer.Append(','); + } + } + + } // class DigestResponse + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/Mechanisms/PlainSaslClient.cs b/qpid/dotnet/Qpid.Sasl/Mechanisms/PlainSaslClient.cs new file mode 100644 index 0000000000..a6813fa825 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Mechanisms/PlainSaslClient.cs @@ -0,0 +1,81 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +namespace Qpid.Sasl.Mechanisms +{ + + /// + /// Implements the PLAIN authentication mechanism + /// as outlined in RFC 4616 + /// + public class PlainSaslClient : SaslClient + { + public const string Mechanism = "PLAIN"; + + public PlainSaslClient( + string authid, IDictionary properties, + ISaslCallbackHandler handler) + : base(authid, null, null, properties, handler) + { + } + + #region ISaslClient Implementation + // + // ISaslClient Implementation + // + + public override string MechanismName + { + get { return Mechanism; } + } + + public override bool HasInitialResponse + { + get { return true; } + } + + public override byte[] EvaluateChallenge(byte[] challenge) + { + // ignore challenge + + NameCallback nameCB = new NameCallback(); + PasswordCallback pwdCB = new PasswordCallback(); + ISaslCallback[] callbacks = { nameCB, pwdCB }; + Handler.Handle(callbacks); + + string username = nameCB.Text; + string authid = AuthorizationId; + string passwd = pwdCB.Text; + + string response = + string.Format("{0}\0{1}\0{2}", authid, username, passwd); + SetComplete(); + return Encoding.UTF8.GetBytes(response); + } + + #endregion // ISaslClient Implementation + + } // class PlainSaslClient +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/Properties/AssemblyInfo.cs b/qpid/dotnet/Qpid.Sasl/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9e422b106e --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Qpid.Sasl")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apache Qpid")] +[assembly: AssemblyProduct("Apache Qpid")] +[assembly: AssemblyCopyright("Copyright © 2007 The Apache Software Foundation")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("27ea23e4-6f84-4a54-8f1f-5725e6d767cc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: CLSCompliant(true)] diff --git a/qpid/dotnet/Qpid.Sasl/Qpid.Sasl.csproj b/qpid/dotnet/Qpid.Sasl/Qpid.Sasl.csproj new file mode 100644 index 0000000000..fa7e91811f --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Qpid.Sasl.csproj @@ -0,0 +1,63 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {1465B0EE-6452-42A6-AB73-B2F9EABEEE75} + Library + Properties + Qpid.Sasl + Qpid.Sasl + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qpid/dotnet/Qpid.Sasl/Sasl.cs b/qpid/dotnet/Qpid.Sasl/Sasl.cs new file mode 100644 index 0000000000..baab5c40e6 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/Sasl.cs @@ -0,0 +1,115 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Configuration; +using System.Text; + +using Qpid.Sasl.Configuration; + +namespace Qpid.Sasl +{ + /// + /// Static class used to access the SASL functionality. + /// The core SASL mechanism is described in RFC 2222. + /// + /// + /// Only client side mechanisms are implemented. + /// + /// New client side factories can be added programatically using the + /// RegisterClientFactory method, or through the application + /// configuration file, like this: + /// + /// + /// + ///
+ /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + public sealed class Sasl + { + private static IList _clientFactories; + + + static Sasl() + { + SaslConfiguration config = SaslConfiguration.GetConfiguration(); + _clientFactories = config.ClientFactories; + } + private Sasl() + { + } + + public static ISaslClient CreateClient( + string[] mechanisms, string authorizationId, + string protocol, string serverName, + IDictionary props, ISaslCallbackHandler handler + ) + { + ISaslClientFactory factory = FindFactory(mechanisms, props); + if ( factory == null ) + return null; + + return factory.CreateClient ( + mechanisms, authorizationId, + protocol, serverName, props, handler + ); + } + + public static void RegisterClientFactory(ISaslClientFactory factory) + { + lock ( _clientFactories ) + { + _clientFactories.Add(factory); + } + } + + private static ISaslClientFactory FindFactory(string[] mechanisms, IDictionary props) + { + lock ( _clientFactories ) + { + foreach ( ISaslClientFactory factory in _clientFactories ) + { + string[] mechs = factory.GetSupportedMechanisms(props); + foreach ( string m1 in mechs ) + { + foreach (string m2 in mechanisms ) + { + if ( m1 == m2 ) + return factory; + } + } + } + return null; + } + } + } // class Sasl + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/SaslClient.cs b/qpid/dotnet/Qpid.Sasl/SaslClient.cs new file mode 100644 index 0000000000..79413e0077 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/SaslClient.cs @@ -0,0 +1,145 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Globalization; +using System.Text; + +namespace Qpid.Sasl +{ + public abstract class SaslClient : ISaslClient + { + private bool _isComplete; + private IDictionary _properties; + private string _authorizationId; + private string _serverName; + private string _protocol; + private ISaslCallbackHandler _handler; + + protected string AuthorizationId + { + get { return _authorizationId; } + } + protected string ServerName + { + get { return _serverName; } + } + + protected string Protocol + { + get { return _protocol; } + } + + protected ISaslCallbackHandler Handler + { + get { return _handler; } + } + + protected IDictionary Properties + { + get { return _properties; } + } + + protected SaslClient( + string authid, string serverName, + string protocol, IDictionary properties, + ISaslCallbackHandler handler) + { + if ( properties == null ) + throw new ArgumentNullException("properties"); + if ( handler == null ) + throw new ArgumentNullException("handler"); + + _authorizationId = authid==null ? "" : authid; + _serverName = serverName; + _protocol = protocol; + _properties = properties; + _handler = handler; + + if ( _serverName == null || _serverName.Length == 0 ) + { + _serverName = System.Net.Dns.GetHostName(); + } + } + + + + + #region ISaslClient Implementation + // + // ISaslClient Implementation + // + + public abstract string MechanismName { get; } + + public abstract bool HasInitialResponse { get; } + + public bool IsComplete + { + get { return _isComplete; } + } + + public abstract byte[] EvaluateChallenge(byte[] challenge); + + public virtual object GetNegotiatedProperty(string propName) + { + return null; + } + + public virtual byte[] Unwrap(byte[] buffer, int offset, int length) + { + throw new NotImplementedException(); + } + + public virtual byte[] Wrap(byte[] buffer, int offset, int lenght) + { + throw new NotImplementedException(); + } + + #endregion // ISaslClient Implementation + + + #region Helper Methods + // + // Helper Methods + // + + protected void SetComplete() + { + _isComplete = true; + } + + protected static string ToHex(byte[] buffer) + { + StringBuilder builder = new StringBuilder(); + foreach ( byte b in buffer ) + { + builder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } + + #endregion // Helper Methods + + } // class SaslClient + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/SaslException.cs b/qpid/dotnet/Qpid.Sasl/SaslException.cs new file mode 100644 index 0000000000..203c408c3f --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/SaslException.cs @@ -0,0 +1,56 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Runtime.Serialization; +using System.Text; + +namespace Qpid.Sasl +{ + /// + /// Reports an exception during the processing of an SASL + /// Operation. Only used for authentication-relared errors + /// + [Serializable] + public class SaslException : Exception + { + public SaslException() + { + } + + public SaslException(string message) + : base(message) + { + } + public SaslException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected SaslException(SerializationInfo info, StreamingContext ctxt) + : base(info, ctxt) + { + } + + } // class SaslException + +} // namespace Qpid.Sasl.Mechanisms diff --git a/qpid/dotnet/Qpid.Sasl/SaslProperties.cs b/qpid/dotnet/Qpid.Sasl/SaslProperties.cs new file mode 100644 index 0000000000..0e59ce8f90 --- /dev/null +++ b/qpid/dotnet/Qpid.Sasl/SaslProperties.cs @@ -0,0 +1,42 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; + +namespace Qpid.Sasl +{ + public sealed class SaslProperties + { + public const string PolicyNoPlainText = "NOPLAINTEXT"; + public const string PolicyNoActive = "NOACTIVE"; + public const string PolicyNoDictionary = "NODICTIONARY"; + public const string PolicyNoAnonymous = "NOANONYMOUS"; + public const string PolicyForwardSecrecy = "FORWARD_SECRECY"; + public const string PolicyPassCredentials = "PASS_CREDENTIALS"; + + public const string Qop = "QOP"; + public const string Strength = "STRENGTH"; + + } // class SaslProperties + +} // namespace Qpid.Sasl.Mechanisms -- cgit v1.2.1