diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2008-11-11 20:27:50 -0800 |
---|---|---|
committer | Andrew <andrewarnott@gmail.com> | 2008-11-11 20:27:50 -0800 |
commit | 3e75e9c22a07a079103b53fc38a985ab66384d0d (patch) | |
tree | 2262fbdca4a861129743e9f7784cea061bf22b06 | |
parent | e8ab62f93adad1205fa572285a403e8b91e2ccf5 (diff) | |
download | DotNetOpenAuth-3e75e9c22a07a079103b53fc38a985ab66384d0d.zip DotNetOpenAuth-3e75e9c22a07a079103b53fc38a985ab66384d0d.tar.gz DotNetOpenAuth-3e75e9c22a07a079103b53fc38a985ab66384d0d.tar.bz2 |
Added association messages.
Diffie-Hellman associations seem to be working according to the test.
But lots of refactoring is probably in order.
31 files changed, 1682 insertions, 123 deletions
diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/KeyValueFormEncodingTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/KeyValueFormEncodingTests.cs index e625499..fff1392 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/KeyValueFormEncodingTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/KeyValueFormEncodingTests.cs @@ -67,7 +67,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { } if ((mode & TestMode.Encoder) == TestMode.Encoder) { var e = this.keyValueForm.GetBytes(dict); - Assert.IsTrue(TestUtilities.AreEquivalent(e, kvform), "Encoder did not produced expected result."); + Assert.IsTrue(MessagingUtilities.AreEquivalent(e, kvform), "Encoder did not produced expected result."); } } diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs index 898e1ce..ef0ec53 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs @@ -67,7 +67,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { Assert.AreEqual(expectedContentType, directResponse.Headers[HttpResponseHeader.ContentType]); byte[] actualBytes = new byte[directResponse.ResponseStream.Length]; directResponse.ResponseStream.Read(actualBytes, 0, actualBytes.Length); - Assert.IsTrue(TestUtilities.AreEquivalent(expectedBytes, actualBytes)); + Assert.IsTrue(MessagingUtilities.AreEquivalent(expectedBytes, actualBytes)); } /// <summary> @@ -83,7 +83,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { Response response = new Response { ResponseStream = new MemoryStream(kvf.GetBytes(fields)), }; - Assert.IsTrue(TestUtilities.AreEquivalent(fields, this.accessor.ReadFromResponseInternal(response))); + Assert.IsTrue(MessagingUtilities.AreEquivalent(fields, this.accessor.ReadFromResponseInternal(response))); } } } diff --git a/src/DotNetOpenAuth.Test/OpenId/Messages/AssociateRequestTests.cs b/src/DotNetOpenAuth.Test/OpenId/Messages/AssociateRequestTests.cs index acc885b..ca9b6d6 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Messages/AssociateRequestTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Messages/AssociateRequestTests.cs @@ -14,13 +14,14 @@ namespace DotNetOpenAuth.Test.OpenId.Messages { [TestClass] public class AssociateRequestTests { + private readonly Protocol protocol = Protocol.V20; private Uri secureRecipient = new Uri("https://hi"); private Uri insecureRecipient = new Uri("http://hi"); private AssociateRequest request; [TestInitialize] public void Setup() { - this.request = new AssociateRequest(this.secureRecipient); + this.request = new AssociateUnencryptedRequest(this.secureRecipient); } [TestMethod] @@ -30,38 +31,36 @@ namespace DotNetOpenAuth.Test.OpenId.Messages { [TestMethod] public void Mode() { - Assert.AreEqual("associate", this.request.Mode); + Assert.AreEqual(this.protocol.Args.Mode.associate, this.request.Mode); } [TestMethod] public void MessagePartsTest() { - this.request.AssociationType = "HMAC-SHA1"; - this.request.SessionType = "no-encryption"; + this.request.AssociationType = this.protocol.Args.SignatureAlgorithm.HMAC_SHA1; + this.request.SessionType = this.protocol.Args.SessionType.NoEncryption; - Assert.AreEqual("associate", this.request.Mode); - Assert.AreEqual("HMAC-SHA1", this.request.AssociationType); - Assert.AreEqual("no-encryption", this.request.SessionType); + Assert.AreEqual(this.protocol.Args.Mode.associate, this.request.Mode); + Assert.AreEqual(this.protocol.Args.SignatureAlgorithm.HMAC_SHA1, this.request.AssociationType); + Assert.AreEqual(this.protocol.Args.SessionType.NoEncryption, this.request.SessionType); var dict = new MessageDictionary(this.request); - Assert.AreEqual(Protocol.OpenId2Namespace, dict["openid.ns"]); - Assert.AreEqual("associate", dict["openid.mode"]); - Assert.AreEqual("HMAC-SHA1", dict["openid.assoc_type"]); - Assert.AreEqual("no-encryption", dict["openid.session_type"]); + Assert.AreEqual(Protocol.OpenId2Namespace, dict[this.protocol.openid.ns]); + Assert.AreEqual(this.protocol.Args.Mode.associate, dict[this.protocol.openid.mode]); + Assert.AreEqual(this.protocol.Args.SignatureAlgorithm.HMAC_SHA1, dict[this.protocol.openid.assoc_type]); + Assert.AreEqual(this.protocol.Args.SessionType.NoEncryption, dict[this.protocol.openid.session_type]); } [TestMethod] public void ValidMessageTest() { - this.request = new AssociateRequest(this.secureRecipient); - this.request.AssociationType = "HMAC-SHA1"; - this.request.SessionType = "no-encryption"; + this.request = new AssociateUnencryptedRequest(this.secureRecipient); + this.request.AssociationType = this.protocol.Args.SignatureAlgorithm.HMAC_SHA1; this.request.EnsureValidMessage(); } [TestMethod, ExpectedException(typeof(ProtocolException))] public void InvalidMessageTest() { - this.request = new AssociateRequest(this.insecureRecipient); - this.request.AssociationType = "HMAC-SHA1"; - this.request.SessionType = "no-encryption"; + this.request = new AssociateUnencryptedRequest(this.insecureRecipient); + this.request.AssociationType = this.protocol.Args.SignatureAlgorithm.HMAC_SHA1; this.request.EnsureValidMessage(); // no-encryption only allowed for secure channels. } diff --git a/src/DotNetOpenAuth.Test/OpenId/ScenarioTests.cs b/src/DotNetOpenAuth.Test/OpenId/ScenarioTests.cs index 53a5874..f3d78e8 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ScenarioTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ScenarioTests.cs @@ -10,31 +10,39 @@ namespace DotNetOpenAuth.Test.OpenId { using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.Messages; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class ScenarioTests { + private readonly Protocol Protocol = Protocol.V20; + [TestMethod] - public void Associate() { - // TODO: This is a VERY trivial association scenario that doesn't actually do anything significant. It needs to get beefed up. + public void AssociateDiffieHellmanMessages() { + Association rpAssociation = null, opAssociation = null; OpenIdCoordinator coordinator = new OpenIdCoordinator( rp => { - var associateRequest = new AssociateRequest(new Uri("http://host")); - associateRequest.AssociationType = "HMAC-SHA1"; - associateRequest.SessionType = "DH-SHA1"; - IProtocolMessage responseMessage = rp.Channel.Request(associateRequest); + var associateRequest = new AssociateDiffieHellmanRequest(new Uri("http://host")); + associateRequest.AssociationType = Protocol.Args.SignatureAlgorithm.HMAC_SHA1; + associateRequest.SessionType = Protocol.Args.SessionType.DH_SHA1; + associateRequest.InitializeRequest(); + var associateResponse = rp.Channel.Request<AssociateDiffieHellmanResponse>(associateRequest); + rpAssociation = associateResponse.CreateAssociation(associateRequest); + Assert.IsNotNull(rpAssociation); + Assert.IsFalse(MessagingUtilities.AreEquivalent(associateResponse.EncodedMacKey, rpAssociation.SecretKey), "Key should have been encrypted."); }, op => { - var associateRequest = op.Channel.ReadFromRequest<AssociateRequest>(); - var response = new AssociateUnencryptedResponse(); + var associateRequest = op.Channel.ReadFromRequest<AssociateDiffieHellmanRequest>(); + var response = new AssociateDiffieHellmanResponse(); response.AssociationType = associateRequest.AssociationType; - response.SessionType = associateRequest.SessionType; - response.AssociationHandle = "{somehandle}"; - response.MacKey = new byte[] { 0x22, 0x33, 0x44 }; + opAssociation = response.CreateAssociation(associateRequest); op.Channel.Send(response); }); coordinator.Run(); + Assert.AreEqual(opAssociation.Handle, rpAssociation.Handle); + Assert.IsTrue(Math.Abs(opAssociation.SecondsTillExpiration - rpAssociation.SecondsTillExpiration) < 60); + Assert.IsTrue(MessagingUtilities.AreEquivalent(opAssociation.SecretKey, rpAssociation.SecretKey)); } } } diff --git a/src/DotNetOpenAuth.Test/TestUtilities.cs b/src/DotNetOpenAuth.Test/TestUtilities.cs index 3b70cc2..a2ff689 100644 --- a/src/DotNetOpenAuth.Test/TestUtilities.cs +++ b/src/DotNetOpenAuth.Test/TestUtilities.cs @@ -13,33 +13,5 @@ namespace DotNetOpenAuth.Test { /// An assortment of methods useful for testing. /// </summary> internal class TestUtilities { - /// <summary> - /// Tests whether two arrays are equal in length and contents. - /// </summary> - /// <typeparam name="T">The type of elements in the arrays.</typeparam> - /// <param name="first">The first array to test. May not be null.</param> - /// <param name="second">The second array to test. May not be null.</param> - /// <returns>True if the arrays equal; false otherwise.</returns> - public static bool AreEquivalent<T>(T[] first, T[] second) { - if (first == null) { - throw new ArgumentNullException("first"); - } - if (second == null) { - throw new ArgumentNullException("second"); - } - if (first.Length != second.Length) { - return false; - } - for (int i = 0; i < first.Length; i++) { - if (!first[i].Equals(second[i])) { - return false; - } - } - return true; - } - - public static bool AreEquivalent<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second) { - return AreEquivalent(first.ToArray(), second.ToArray()); - } } } diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index 6707b28..a31ac97 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -34,7 +34,7 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Consumer", "..\samples\Cons Release.AspNetCompiler.ForceOverwrite = "true" Release.AspNetCompiler.FixedNames = "false" Release.AspNetCompiler.Debug = "False" - VWDPort = "33321" + VWDPort = "48147" DefaultWebSiteLanguage = "Visual C#" EndProjectSection ProjectSection(ProjectDependencies) = postProject @@ -63,7 +63,7 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "ServiceProvider", "..\sampl Release.AspNetCompiler.ForceOverwrite = "true" Release.AspNetCompiler.FixedNames = "false" Release.AspNetCompiler.Debug = "False" - VWDPort = "37480" + VWDPort = "48149" DefaultWebSiteLanguage = "Visual C#" EndProjectSection EndProject diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 7cef132..216d128 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -143,9 +143,14 @@ <Compile Include="Messaging\MessageTransport.cs" /> <Compile Include="OAuth\ChannelElements\OAuthServiceProviderMessageTypeProvider.cs" /> <Compile Include="Messaging\ProtocolException.cs" /> + <Compile Include="OpenId\Association.cs" /> + <Compile Include="OpenId\ChannelElements\ITamperResistantOpenIdMessage.cs" /> + <Compile Include="OpenId\ChannelElements\SigningBindingElement.cs" /> <Compile Include="OpenId\ChannelElements\KeyValueFormEncoding.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdChannel.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdMessageTypeProvider.cs" /> + <Compile Include="OpenId\Configuration.cs" /> + <Compile Include="OpenId\DiffieHellmanUtilities.cs" /> <Compile Include="OpenId\DiffieHellman\DHKeyGeneration.cs" /> <Compile Include="OpenId\DiffieHellman\DHParameters.cs" /> <Compile Include="OpenId\DiffieHellman\DiffieHellman.cs" /> @@ -156,6 +161,9 @@ <Compile Include="OpenId\DiffieHellman\mono\PrimalityTests.cs" /> <Compile Include="OpenId\DiffieHellman\mono\PrimeGeneratorBase.cs" /> <Compile Include="OpenId\DiffieHellman\mono\SequentialSearchPrimeGeneratorBase.cs" /> + <Compile Include="OpenId\HmacShaAssociation.cs" /> + <Compile Include="OpenId\IAssociationStore.cs" /> + <Compile Include="OpenId\Messages\AssociateUnencryptedRequest.cs" /> <Compile Include="OpenId\OpenIdProvider.cs" /> <Compile Include="OpenId\Messages\AssociateDiffieHellmanRequest.cs" /> <Compile Include="OpenId\Messages\AssociateDiffieHellmanResponse.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index cd3cba1..eb6e4f8 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -376,6 +376,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to The array must not be empty.. + /// </summary> + internal static string UnexpectedEmptyArray { + get { + return ResourceManager.GetString("UnexpectedEmptyArray", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The empty string is not allowed.. /// </summary> internal static string UnexpectedEmptyString { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 5f5b9b4..406e0ff 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -222,6 +222,9 @@ <data name="TooManyBindingsOfferingSameProtection" xml:space="preserve"> <value>Expected at most 1 binding element offering the {0} protection, but found {1}.</value> </data> + <data name="UnexpectedEmptyArray" xml:space="preserve"> + <value>The array must not be empty.</value> + </data> <data name="UnexpectedEmptyString" xml:space="preserve"> <value>The empty string is not allowed.</value> </data> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 0efb072..af59b40 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.Messaging { using System.IO; using System.Linq; using System.Net; + using System.Security.Cryptography; using System.Text; using System.Web; using DotNetOpenAuth.Messaging.Reflection; @@ -20,6 +21,12 @@ namespace DotNetOpenAuth.Messaging { /// </summary> public static class MessagingUtilities { /// <summary> + /// The cryptographically strong random data generator used for creating secrets. + /// </summary> + /// <remarks>The random number generator is thread-safe.</remarks> + internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); + + /// <summary> /// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any. /// Cookieless session directory (if applicable) is also included. /// </summary> @@ -62,6 +69,17 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a cryptographically strong random sequence of values. + /// </summary> + /// <param name="length">The length of the sequence to generate.</param> + /// <returns>The generated values, which may contain zeros.</returns> + internal static byte[] GetCryptoRandomData(int length) { + byte[] buffer = new byte[length]; + CryptoRandomDataGenerator.GetBytes(buffer); + return buffer; + } + + /// <summary> /// Adds a set of HTTP headers to an <see cref="HttpResponse"/> instance, /// taking care to set some headers to the appropriate properties of /// <see cref="HttpResponse" /> @@ -120,6 +138,43 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Tests whether two arrays are equal in length and contents. + /// </summary> + /// <typeparam name="T">The type of elements in the arrays.</typeparam> + /// <param name="first">The first array in the comparison. May not be null.</param> + /// <param name="second">The second array in the comparison. May not be null.</param> + /// <returns>True if the arrays equal; false otherwise.</returns> + internal static bool AreEquivalent<T>(T[] first, T[] second) { + if (first == null) { + throw new ArgumentNullException("first"); + } + if (second == null) { + throw new ArgumentNullException("second"); + } + if (first.Length != second.Length) { + return false; + } + for (int i = 0; i < first.Length; i++) { + if (!first[i].Equals(second[i])) { + return false; + } + } + return true; + } + + /// <summary> + /// Tests whether two dictionaries are equal in length and contents. + /// </summary> + /// <typeparam name="TKey">The type of keys in the dictionaries.</typeparam> + /// <typeparam name="TValue">The type of values in the dictionaries.</typeparam> + /// <param name="first">The first dictionary in the comparison. May not be null.</param> + /// <param name="second">The second dictionary in the comparison. May not be null.</param> + /// <returns>True if the arrays equal; false otherwise.</returns> + internal static bool AreEquivalent<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second) { + return AreEquivalent(first.ToArray(), second.ToArray()); + } + + /// <summary> /// Concatenates a list of name-value pairs as key=value&key=value, /// taking care to properly encode each key and value for URL /// transmission. No ? is prefixed to the string. diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index ca55746..fafefde 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -299,14 +299,15 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// Fills out the secrets in a message so that signing/verification can be performed. /// </summary> /// <param name="message">The message about to be signed or whose signature is about to be verified.</param> - private void SignatureCallback(ITamperResistantOAuthMessage message) { + private void SignatureCallback(ITamperResistantProtocolMessage message) { + var oauthMessage = message as ITamperResistantOAuthMessage; try { Logger.Debug("Applying secrets to message to prepare for signing or signature verification."); - message.ConsumerSecret = this.TokenManager.GetConsumerSecret(message.ConsumerKey); + oauthMessage.ConsumerSecret = this.TokenManager.GetConsumerSecret(oauthMessage.ConsumerKey); var tokenMessage = message as ITokenContainingMessage; if (tokenMessage != null) { - message.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token); + oauthMessage.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token); } } catch (KeyNotFoundException ex) { throw new ProtocolException(OAuthStrings.ConsumerOrTokenSecretNotFound, ex); diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/StandardTokenGenerator.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/StandardTokenGenerator.cs index 1d2e3af..6d399a4 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/StandardTokenGenerator.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/StandardTokenGenerator.cs @@ -7,16 +7,12 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System; using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; /// <summary> /// A cryptographically strong random string generator for tokens and secrets. /// </summary> internal class StandardTokenGenerator : ITokenGenerator { - /// <summary> - /// The cryptographically strong random string generator for tokens and secrets. - /// </summary> - private RandomNumberGenerator cryptoProvider = new RNGCryptoServiceProvider(); - #region ITokenGenerator Members /// <summary> @@ -61,7 +57,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <returns>The new random string.</returns> private string GenerateCryptographicallyStrongString() { byte[] buffer = new byte[20]; - this.cryptoProvider.GetBytes(buffer); + MessagingUtilities.CryptoRandomDataGenerator.GetBytes(buffer); return Convert.ToBase64String(buffer); } } diff --git a/src/DotNetOpenAuth/OpenId/Association.cs b/src/DotNetOpenAuth/OpenId/Association.cs new file mode 100644 index 0000000..50308d1 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Association.cs @@ -0,0 +1,280 @@ +//----------------------------------------------------------------------- +// <copyright file="Association.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Stores a secret used in signing and verifying messages. + /// </summary> + /// <remarks> + /// OpenID associations may be shared between Provider and Relying Party (smart + /// associations), or be a way for a Provider to recall its own secret for later + /// (dumb associations). + /// </remarks> + [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")] + public abstract class Association { + /// <summary> + /// The duration any association and secret key the Provider generates will be good for. + /// </summary> + protected static readonly TimeSpan SmartAssociationLifetime = TimeSpan.FromDays(14); + + /// <summary> + /// The duration a secret key used for signing dumb client requests will be good for. + /// </summary> + protected static readonly TimeSpan DumbSecretLifetime = TimeSpan.FromMinutes(5); + + /// <summary> + /// Initializes a new instance of the <see cref="Association"/> class. + /// </summary> + /// <param name="handle">The handle.</param> + /// <param name="secret">The secret.</param> + /// <param name="totalLifeLength">How long the association will be useful.</param> + /// <param name="issued">When this association was originally issued by the Provider.</param> + protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) { + if (string.IsNullOrEmpty(handle)) { + throw new ArgumentNullException("handle"); + } + if (secret == null) { + throw new ArgumentNullException("secret"); + } + this.Handle = handle; + this.SecretKey = secret; + this.TotalLifeLength = totalLifeLength; + this.Issued = CutToSecond(issued); + } + + /// <summary> + /// Gets a unique handle by which this <see cref="Association"/> may be stored or retrieved. + /// </summary> + public string Handle { get; private set; } + + /// <summary> + /// Gets the time when this <see cref="Association"/> will expire. + /// </summary> + public DateTime Expires { + get { return this.Issued + this.TotalLifeLength; } + } + + /// <summary> + /// Gets a value indicating whether this <see cref="Association"/> has already expired. + /// </summary> + public bool IsExpired { + get { return this.Expires < DateTime.UtcNow; } + } + + /// <summary> + /// Gets a value indicating whether this instance has useful life remaining. + /// </summary> + /// <value> + /// <c>true</c> if this instance has useful life remaining; otherwise, <c>false</c>. + /// </value> + internal bool HasUsefulLifeRemaining { + get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; } + } + + /// <summary> + /// Gets or sets the time that this <see cref="Association"/> was first created. + /// </summary> + internal DateTime Issued { get; set; } + + /// <summary> + /// Gets the number of seconds until this <see cref="Association"/> expires. + /// Never negative (counter runs to zero). + /// </summary> + protected internal long SecondsTillExpiration { + get { return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); } + } + + /// <summary> + /// Gets the shared secret key between the consumer and provider. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It is a buffer.")] + protected internal byte[] SecretKey { get; private set; } + + /// <summary> + /// Gets the lifetime the OpenID provider permits this <see cref="Association"/>. + /// </summary> + protected TimeSpan TotalLifeLength { get; private set; } + + /// <summary> + /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication. + /// </summary> + /// <remarks> + /// Associations that are not likely to last the duration of a user login are not worth using at all. + /// </remarks> + private static TimeSpan MinimumUsefulAssociationLifetime { + get { return Configuration.MaximumUserAgentAuthenticationTime; } + } + + /// <summary> + /// Gets the TimeSpan till this association expires. + /// </summary> + private TimeSpan TimeTillExpiration { + get { return this.Expires - DateTime.UtcNow; } + } + + /// <summary> + /// Re-instantiates an <see cref="Association"/> previously persisted in a database or some + /// other shared store. + /// </summary> + /// <param name="handle"> + /// The <see cref="Handle"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="expires"> + /// The value of the <see cref="Expires"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="privateData"> + /// The byte array returned by a call to <see cref="SerializePrivateData"/> on the previous + /// <see cref="Association"/> instance. + /// </param> + /// <returns> + /// The newly dehydrated <see cref="Association"/>, which can be returned + /// from a custom association store's + /// <see cref="IAssociationStore<TKey>.GetAssociation(TKey)"/> method. + /// </returns> + public static Association Deserialize(string handle, DateTime expires, byte[] privateData) { + if (string.IsNullOrEmpty(handle)) { + throw new ArgumentNullException("handle"); + } + if (privateData == null) { + throw new ArgumentNullException("privateData"); + } + expires = expires.ToUniversalTime(); + TimeSpan remainingLifeLength = expires - DateTime.UtcNow; + byte[] secret = privateData; // the whole of privateData is the secret key for now. + // We figure out what derived type to instantiate based on the length of the secret. + try { + return HmacShaAssociation.Create(secret.Length, handle, secret, remainingLifeLength); + } catch (ArgumentException ex) { + throw new ArgumentException(OpenIdStrings.BadAssociationPrivateData, "privateData", ex); + } + } + + /// <summary> + /// Returns private data required to persist this <see cref="Association"/> in + /// permanent storage (a shared database for example) for deserialization later. + /// </summary> + /// <returns> + /// An opaque byte array that must be stored and returned exactly as it is provided here. + /// The byte array may vary in length depending on the specific type of <see cref="Association"/>, + /// but in current versions are no larger than 256 bytes. + /// </returns> + /// <remarks> + /// Values of public properties on the base class <see cref="Association"/> are not included + /// in this byte array, as they are useful for fast database lookup and are persisted separately. + /// </remarks> + public byte[] SerializePrivateData() { + // We may want to encrypt this secret using the machine.config private key, + // and add data regarding which Association derivative will need to be + // re-instantiated on deserialization. + // For now, we just send out the secret key. We can derive the type from the length later. + byte[] secretKeyCopy = new byte[this.SecretKey.Length]; + this.SecretKey.CopyTo(secretKeyCopy, 0); + return secretKeyCopy; + } + + /// <summary> + /// Tests equality of two <see cref="Association"/> objects. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param> + /// <returns> + /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false. + /// </returns> + public override bool Equals(object obj) { + Association a = obj as Association; + if (a == null) { + return false; + } + if (a.GetType() != GetType()) { + return false; + } + + if (a.Handle != this.Handle || + a.Issued != this.Issued || + a.TotalLifeLength != this.TotalLifeLength) { + return false; + } + + if (!MessagingUtilities.AreEquivalent(a.SecretKey, this.SecretKey)) { + return false; + } + + return true; + } + + /// <summary> + /// Returns the hash code. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + HMACSHA1 hmac = new HMACSHA1(this.SecretKey); + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); + + byte[] hash = hmac.Hash; + hmac.Clear(); + + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } + + val = val ^ this.Expires.ToFileTimeUtc(); + + return (int)val; + } + + /// <summary> + /// The string to pass as the assoc_type value in the OpenID protocol. + /// </summary> + /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param> + /// <returns>The value that should be used for the openid.assoc_type parameter.</returns> + internal abstract string GetAssociationType(Protocol protocol); + + /// <summary> + /// Generates a signature from a given blob of data. + /// </summary> + /// <param name="data">The data to sign. This data will not be changed (the signature is the return value).</param> + /// <returns>The calculated signature of the data.</returns> + protected internal byte[] Sign(byte[] data) { + using (HashAlgorithm hasher = this.CreateHasher()) { + return hasher.ComputeHash(data); + } + } + + /// <summary> + /// Returns the specific hash algorithm used for message signing. + /// </summary> + /// <returns>The hash algorithm used for message signing.</returns> + protected abstract HashAlgorithm CreateHasher(); + + /// <summary> + /// Rounds the given <see cref="DateTime"/> downward to the whole second. + /// </summary> + /// <param name="dateTime">The DateTime object to adjust.</param> + /// <returns>The new <see cref="DateTime"/> value.</returns> + private static DateTime CutToSecond(DateTime dateTime) { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond)); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs new file mode 100644 index 0000000..3c0ecad --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="ITamperResistantOpenIdMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An interface that OAuth messages implement to support signing. + /// </summary> + internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage { + /// <summary> + /// Gets or sets the association handle used to sign the message. + /// </summary> + string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the signed parameter order. + /// </summary> + /// <value>Comma-separated list of signed fields.</value> + /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example> + /// <remarks> + /// This entry consists of the fields without the "openid." prefix that the signature covers. + /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle", + /// and if present in the response, "claimed_id" and "identity". + /// Additional keys MAY be signed as part of the message. See Generating Signatures. + /// </remarks> + string SignedParameterOrder { get; set; } + } +} diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/KeyValueFormEncoding.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/KeyValueFormEncoding.cs index c9210d8..c6f5508 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/KeyValueFormEncoding.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/KeyValueFormEncoding.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { using System.Globalization; using System.IO; using System.Text; + using DotNetOpenAuth.Messaging; /// <summary> /// Indicates the level of strictness to require when decoding a @@ -87,60 +88,33 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// <summary> /// Encodes key/value pairs to Key-Value Form. - /// Do not use for dictionaries of signed fields! Instead use the overload - /// that accepts a list of in-order keys. /// </summary> - /// <param name="dictionary">The dictionary with key/value pairs to encode in Key-Value Form.</param> - /// <returns>The UTF8 byte array.</returns> - /// <remarks> - /// Because dictionaries do not guarantee ordering, - /// encoding a dictionary without an explicitly given key order - /// is useless in OpenID scenarios where a signature must match. - /// </remarks> - public byte[] GetBytes(IDictionary<string, string> dictionary) { - string[] keys = new string[dictionary.Count]; - dictionary.Keys.CopyTo(keys, 0); - return this.GetBytes(dictionary, keys); - } - - /// <summary> - /// Encodes key/value pairs to Key-Value Form. - /// </summary> - /// <param name="dictionary"> + /// <param name="keysAndValues"> /// The dictionary of key/value pairs to convert to a byte stream. /// </param> - /// <param name="keyOrder"> - /// The order in which to encode the key/value pairs. - /// Useful in scenarios where a byte[] must be exactly reproduced. - /// </param> /// <returns>The UTF8 byte array.</returns> - public byte[] GetBytes(IDictionary<string, string> dictionary, IList<string> keyOrder) { - if (dictionary == null) { - throw new ArgumentNullException("dictionary"); - } - if (keyOrder == null) { - throw new ArgumentNullException("keyOrder"); - } - if (dictionary.Count != keyOrder.Count) { - throw new ArgumentException(OpenIdStrings.KeysListAndDictionaryDoNotMatch); - } + /// <remarks> + /// Enumerating a Dictionary<TKey, TValue> has undeterministic ordering. + /// If ordering of the key=value pairs is important, a deterministic enumerator must + /// be used. + /// </remarks> + public byte[] GetBytes(IEnumerable<KeyValuePair<string, string>> keysAndValues) { + ErrorUtilities.VerifyArgumentNotNull(keysAndValues, "keysAndValues"); MemoryStream ms = new MemoryStream(); using (StreamWriter sw = new StreamWriter(ms, textEncoding)) { sw.NewLine = NewLineCharacters; - foreach (string keyInOrder in keyOrder) { - string key = keyInOrder.Trim(); - string value = dictionary[key].Trim(); - if (key.IndexOfAny(IllegalKeyCharacters) >= 0) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, key)); + foreach (var pair in keysAndValues) { + if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key)); } - if (value.IndexOfAny(IllegalValueCharacters) >= 0) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, value)); + if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value)); } - sw.Write(key); + sw.Write(pair.Key); sw.Write(':'); - sw.Write(value); + sw.Write(pair.Value); sw.WriteLine(); } } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/SigningBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/SigningBindingElement.cs new file mode 100644 index 0000000..624b157 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/SigningBindingElement.cs @@ -0,0 +1,151 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// Signs and verifies authentication assertions. + /// </summary> + /// <typeparam name="TKey"> + /// <see cref="System.Uri"/> for consumers (to distinguish associations across servers) or + /// <see cref="AssociationRelyingPartyType"/> for providers (to distinguish dumb and smart client associations). + /// </typeparam> + internal class SigningBindingElement<TKey> : IChannelBindingElement { + /// <summary> + /// The association store used to look up the secrets needed for signing. + /// </summary> + private IAssociationStore<TKey> associations; + + /// <summary> + /// Initializes a new instance of the SigningBindingElement class. + /// </summary> + /// <param name="associations">The association store used to look up the secrets needed for signing.</param> + internal SigningBindingElement(IAssociationStore<TKey> associations) { + ErrorUtilities.VerifyArgumentNotNull(associations, "associations"); + + this.associations = associations; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.TamperProtection"/></value> + public MessageProtections Protection { + get { return MessageProtections.TamperProtection; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False otherwise. + /// </returns> + public bool PrepareMessageForSending(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.DebugFormat("Signing {0} message.", message.GetType().Name); + if (string.IsNullOrEmpty(signedMessage.AssociationHandle)) { + // TODO: code here + ////signedMessage.AssociationHandle = + } + signedMessage.SignedParameterOrder = this.GetSignedParameterOrder(signedMessage); + signedMessage.Signature = this.GetSignature(signedMessage); + return true; + } + + return false; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False if the operation did not apply to this message. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public bool PrepareMessageForReceiving(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature); + + string signature = this.GetSignature(signedMessage); + if (!string.Equals(signedMessage.Signature, signature, StringComparison.Ordinal)) { + Logger.Error("Signature verification failed."); + throw new InvalidSignatureException(message); + } + + return true; + } + + return false; + } + + #endregion + + /// <summary> + /// Gets the value to use for the openid.signed parameter. + /// </summary> + /// <param name="signedMessage">The signable message.</param> + /// <returns> + /// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines + /// the inclusion and order of message parts that will be signed. + /// </returns> + private string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) { + ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage"); + + MessageDescription description = MessageDescription.Get(signedMessage.GetType()); + var signedParts = from part in description.Mapping.Values + where (part.RequiredProtection & System.Net.Security.ProtectionLevel.Sign) != 0 + select part.Name; + string prefix = Protocol.V20.openid.Prefix; + Debug.Assert(signedParts.All(name => name.StartsWith(prefix, StringComparison.Ordinal)), "All signed message parts must start with 'openid.'."); + int skipLength = prefix.Length; + string signedFields = string.Join(",", signedParts.Select(name => name.Substring(skipLength)).ToArray()); + return signedFields; + } + + /// <summary> + /// Calculates the signature for a given message. + /// </summary> + /// <param name="signedMessage">The message to sign.</param> + /// <returns>The calculated signature of the method.</returns> + private string GetSignature(ITamperResistantOpenIdMessage signedMessage) { + ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage"); + ErrorUtilities.VerifyNonZeroLength(signedMessage.SignedParameterOrder, "signedMessage.SignedParameterOrder"); + + MessageDictionary dictionary = new MessageDictionary(signedMessage); + var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',') + let prefixedName = Protocol.V20.openid.Prefix + name + select new KeyValuePair<string, string>(prefixedName, dictionary[prefixedName]); + + KeyValueFormEncoding keyValueForm = new KeyValueFormEncoding(); + byte[] dataToSign = keyValueForm.GetBytes(parametersToSign); + + Association association = null; // TODO: fetch the association to use + string signature = Convert.ToBase64String(association.Sign(dataToSign)); + return signature; + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Configuration.cs b/src/DotNetOpenAuth/OpenId/Configuration.cs new file mode 100644 index 0000000..b3cbb35 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Configuration.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="Configuration.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// A set of adjustable properties that control various aspects of OpenID behavior. + /// </summary> + internal static class Configuration { + /// <summary> + /// Initializes static members of the <see cref="Configuration"/> class. + /// </summary> + static Configuration() { + MaximumUserAgentAuthenticationTime = TimeSpan.FromMinutes(5); + } + + /// <summary> + /// Gets the maximum time a user can be allowed to take to complete authentication. + /// </summary> + /// <remarks> + /// This is used to calculate the length of time that nonces are stored. + /// This is internal until we can decide whether to leave this static, or make + /// it an instance member, or put it inside the IConsumerApplicationStore interface. + /// </remarks> + internal static TimeSpan MaximumUserAgentAuthenticationTime { get; private set; } + } +} diff --git a/src/DotNetOpenAuth/OpenId/DiffieHellmanUtilities.cs b/src/DotNetOpenAuth/OpenId/DiffieHellmanUtilities.cs new file mode 100644 index 0000000..43306b5 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/DiffieHellmanUtilities.cs @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------- +// <copyright file="DiffieHellmanUtilities.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using Org.Mentalis.Security.Cryptography; + + /// <summary> + /// Diffie-Hellman encryption methods used by both the relying party and provider. + /// </summary> + internal class DiffieHellmanUtilities { + private static DHSha[] diffieHellmanSessionTypes = { + new DHSha(new SHA512Managed(), protocol => protocol.Args.SessionType.DH_SHA512), + new DHSha(new SHA384Managed(), protocol => protocol.Args.SessionType.DH_SHA384), + new DHSha(new SHA256Managed(), protocol => protocol.Args.SessionType.DH_SHA256), + new DHSha(new SHA1Managed(), protocol => protocol.Args.SessionType.DH_SHA1), + }; + + public static HashAlgorithm Lookup(Protocol protocol, string sessionType) { + ErrorUtilities.VerifyArgumentNotNull(protocol, "protocol"); + ErrorUtilities.VerifyArgumentNotNull(sessionType, "sessionType"); + + foreach (DHSha dhsha in diffieHellmanSessionTypes) { + if (String.Equals(dhsha.GetName(protocol), sessionType, StringComparison.Ordinal)) { + return dhsha.Algorithm; + } + } + throw new ArgumentOutOfRangeException("name"); + } + + internal static string GetNameForSize(Protocol protocol, int hashSizeInBits) { + ErrorUtilities.VerifyArgumentNotNull(protocol, "protocol"); + foreach (DHSha dhsha in diffieHellmanSessionTypes) { + if (dhsha.Algorithm.HashSize == hashSizeInBits) { + return dhsha.GetName(protocol); + } + } + return null; + } + + /// <summary> + /// Encrypts/decrypts a shared secret. + /// </summary> + /// <param name="hasher">The hashing algorithm that is agreed by both parties to use as part of the secret exchange.</param> + /// <param name="dh"> + /// If the secret is being encrypted, this is the new Diffie Hellman object to use. + /// If the secret is being decrypted, this must be the same Diffie Hellman object used to send the original request message. + /// </param> + /// <param name="remotePublicKey">The public key of the remote party.</param> + /// <param name="plainOrEncryptedSecret">The secret to encode, or the encoded secret. Whichever one is given will generate the opposite in the return value.</param> + /// <returns> + /// The encrypted version of the secret if the secret itself was given in <paramref name="remotePublicKey"/>. + /// The secret itself if the encrypted version of the secret was given in <paramref name="remotePublicKey"/>. + /// </returns> + internal static byte[] SHAHashXorSecret(HashAlgorithm hasher, DiffieHellman dh, byte[] remotePublicKey, byte[] plainOrEncryptedSecret) { + ErrorUtilities.VerifyArgumentNotNull(hasher, "hasher"); + ErrorUtilities.VerifyArgumentNotNull(dh, "dh"); + ErrorUtilities.VerifyArgumentNotNull(remotePublicKey, "remotePublicKey"); + ErrorUtilities.VerifyArgumentNotNull(plainOrEncryptedSecret, "plainOrEncryptedSecret"); + + byte[] dhShared = dh.DecryptKeyExchange(remotePublicKey); + byte[] shaDhShared = hasher.ComputeHash(EnsurePositive(dhShared)); + if (shaDhShared.Length != plainOrEncryptedSecret.Length) { + throw new ArgumentOutOfRangeException(string.Format(CultureInfo.CurrentCulture, + "encMacKey's length ({0}) does not match the length of the hashing algorithm ({1}).", + plainOrEncryptedSecret.Length, shaDhShared.Length)); + } + + byte[] secret = new byte[plainOrEncryptedSecret.Length]; + for (int i = 0; i < plainOrEncryptedSecret.Length; i++) { + secret[i] = (byte)(plainOrEncryptedSecret[i] ^ shaDhShared[i]); + } + return secret; + } + + /// <summary> + /// Ensures that the big integer represented by a given series of bytes + /// is a positive integer. + /// </summary> + /// <param name="inputBytes">The bytes that make up the big integer.</param> + /// <returns> + /// A byte array (possibly new if a change was required) whose + /// integer is guaranteed to be positive. + /// </returns> + /// <remarks> + /// This is to be consistent with OpenID spec section 4.2. + /// </remarks> + internal static byte[] EnsurePositive(byte[] inputBytes) { + ErrorUtilities.VerifyArgumentNotNull(inputBytes, "inputBytes"); + if (inputBytes.Length == 0) { + throw new ArgumentException(MessagingStrings.UnexpectedEmptyArray, "inputBytes"); + } + + int i = (int)inputBytes[0]; + if (i > 127) { + byte[] nowPositive = new byte[inputBytes.Length + 1]; + nowPositive[0] = 0; + inputBytes.CopyTo(nowPositive, 1); + return nowPositive; + } + + return inputBytes; + } + + private class DHSha { + public DHSha(HashAlgorithm algorithm, Func<Protocol, string> getName) { + ErrorUtilities.VerifyArgumentNotNull(algorithm, "algorithm"); + ErrorUtilities.VerifyArgumentNotNull(getName, "getName"); + + this.GetName = getName; + this.Algorithm = algorithm; + } + + internal Func<Protocol, string> GetName { get; set; } + + internal HashAlgorithm Algorithm { get; private set; } + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs new file mode 100644 index 0000000..7cc6213 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs @@ -0,0 +1,195 @@ +//----------------------------------------------------------------------- +// <copyright file="HmacShaAssociation.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Diagnostics; + using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Messages; + + internal class HmacShaAssociation : Association { + private static HmacSha[] hmacShaAssociationTypes = { + new HmacSha { + CreateHasher = secretKey => new HMACSHA512(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA512, + BaseHashAlgorithm = new SHA512Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA384(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA384, + BaseHashAlgorithm = new SHA384Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA256(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA256, + BaseHashAlgorithm = new SHA256Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA1(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA1, + BaseHashAlgorithm = new SHA1Managed(), + }, + }; + + private HmacSha typeIdentity; + + /// <summary> + /// Initializes a new instance of the <see cref="HmacShaAssociation"/> class. + /// </summary> + /// <param name="typeIdentity"></param> + /// <param name="handle">The association handle.</param> + /// <param name="secret">The association secret.</param> + /// <param name="totalLifeLength">The time duration the association will be good for.</param> + private HmacShaAssociation(HmacSha typeIdentity, string handle, byte[] secret, TimeSpan totalLifeLength) + : base(handle, secret, totalLifeLength, DateTime.UtcNow) { + ErrorUtilities.VerifyArgumentNotNull(typeIdentity, "typeIdentity"); + + Debug.Assert(secret.Length == typeIdentity.SecretLength); + this.typeIdentity = typeIdentity; + } + + public static HmacShaAssociation Create(Protocol protocol, string associationType, string handle, byte[] secret, TimeSpan totalLifeLength) { + foreach (HmacSha shaType in hmacShaAssociationTypes) { + if (String.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal)) { + return new HmacShaAssociation(shaType, handle, secret, totalLifeLength); + } + } + throw new ArgumentOutOfRangeException("associationType"); + } + + public static HmacShaAssociation Create(int secretLength, string handle, byte[] secret, TimeSpan totalLifeLength) { + foreach (HmacSha shaType in hmacShaAssociationTypes) { + if (shaType.SecretLength == secretLength) { + return new HmacShaAssociation(shaType, handle, secret, totalLifeLength); + } + } + throw new ArgumentOutOfRangeException("secretLength"); + } + + /// <summary> + /// Creates a new association of a given type. + /// </summary> + /// <param name="protocol">The protocol.</param> + /// <param name="associationType">Type of the association.</param> + /// <param name="associationUse"> + /// A value indicating whether the new association will be used privately by the Provider for "dumb mode" authentication + /// or shared with the Relying Party for "smart mode" authentication. + /// </param> + /// <returns>The newly created association.</returns> + /// <remarks> + /// The new association is NOT automatically put into an association store. This must be done by the caller. + /// </remarks> + internal static HmacShaAssociation Create(Protocol protocol, string associationType, AssociationRelyingPartyType associationUse) { + // Generate the handle. It must be unique, so we use a time element and a random data element to generate it. + byte[] uniq_bytes = MessagingUtilities.GetCryptoRandomData(4); + string uniq = Convert.ToBase64String(uniq_bytes); + string handle = "{" + associationType + "}{" + DateTime.UtcNow.Ticks + "}{" + uniq + "}"; + + // Generate the secret that will be used for signing + int secretLength = GetSecretLength(protocol, associationType); + byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength); + + TimeSpan lifetime = associationUse == AssociationRelyingPartyType.Smart ? SmartAssociationLifetime : DumbSecretLifetime; + + return Create(protocol, associationType, handle, secret, lifetime); + } + + /// <summary> + /// Returns the length of the shared secret (in bytes). + /// </summary> + /// <param name="protocol">The protocol version being used that will be used to lookup the text in <paramref name="associationType"/></param> + /// <param name="associationType">The value of the protocol argument specifying the type of association. For example: "HMAC-SHA1".</param> + /// <returns>The length (in bytes) of the association secret.</returns> + public static int GetSecretLength(Protocol protocol, string associationType) { + foreach (HmacSha shaType in hmacShaAssociationTypes) { + if (String.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal)) { + return shaType.SecretLength; + } + } + throw new ArgumentOutOfRangeException("associationType"); + } + + /// <summary> + /// Looks for the longest hash length for a given protocol for which we have an association, + /// and perhaps a matching Diffie-Hellman session type. + /// </summary> + /// <param name="protocol">The OpenID version that dictates which associations are available.</param> + /// <param name="minimumHashSizeInBits">The minimum required hash length given security settings.</param> + /// <param name="maximumHashSizeInBits">The maximum hash length to even attempt. Useful for the RP side where we support SHA512 but most OPs do not -- why waste time trying?</param> + /// <param name="requireMatchingDHSessionType">True for HTTP associations, False for HTTPS associations.</param> + /// <param name="associationType">The resulting association type's well known protocol name. (i.e. HMAC-SHA256)</param> + /// <param name="sessionType">The resulting session type's well known protocol name, if a matching one is available. (i.e. DH-SHA256)</param> + /// <returns>True if a qualifying association could be found; false otherwise.</returns> + internal static bool TryFindBestAssociation(Protocol protocol, int? minimumHashSizeInBits, int? maximumHashSizeInBits, bool requireMatchingDHSessionType, out string associationType, out string sessionType) { + ErrorUtilities.VerifyArgumentNotNull(protocol, "protocol"); + associationType = null; + sessionType = null; + + // We assume this enumeration is in decreasing bit length order. + foreach (HmacSha sha in hmacShaAssociationTypes) { + int hashSizeInBits = sha.SecretLength * 8; + if (maximumHashSizeInBits.HasValue && hashSizeInBits > maximumHashSizeInBits.Value) { + continue; + } + if (minimumHashSizeInBits.HasValue && hashSizeInBits < minimumHashSizeInBits.Value) { + break; + } + sessionType = DiffieHellmanUtilities.GetNameForSize(protocol, hashSizeInBits); + if (requireMatchingDHSessionType && sessionType == null) { + continue; + } + associationType = sha.GetAssociationType(protocol); + return true; + } + return false; + } + + internal static bool IsDHSessionCompatible(Protocol protocol, string associationType, string sessionType) { + // Under HTTPS, no DH encryption is required regardless of association type. + if (string.Equals(sessionType, protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal)) { + return true; + } + + // When there _is_ a DH session, it must match in hash length with the association type. + foreach (HmacSha sha in hmacShaAssociationTypes) { + if (string.Equals(associationType, sha.GetAssociationType(protocol), StringComparison.Ordinal)) { + int hashSizeInBits = sha.SecretLength * 8; + string matchingSessionName = DiffieHellmanUtilities.GetNameForSize(protocol, hashSizeInBits); + if (string.Equals(sessionType, matchingSessionName, StringComparison.Ordinal)) { + return true; + } + } + } + return false; + } + + internal override string GetAssociationType(Protocol protocol) { + return this.typeIdentity.GetAssociationType(protocol); + } + + protected override HashAlgorithm CreateHasher() { + return this.typeIdentity.CreateHasher(SecretKey); + } + + private class HmacSha { + /// <summary> + /// Gets or sets the function that takes a particular OpenID version and returns the name of the association in that protocol. + /// </summary> + internal Func<Protocol, string> GetAssociationType { get; set; } + + internal Func<byte[], HashAlgorithm> CreateHasher { get; set; } + + internal HashAlgorithm BaseHashAlgorithm { get; set; } + + /// <summary> + /// Gets the size of the hash (in bytes). + /// </summary> + internal int SecretLength { get { return this.BaseHashAlgorithm.HashSize / 8; } } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/IAssociationStore.cs b/src/DotNetOpenAuth/OpenId/IAssociationStore.cs new file mode 100644 index 0000000..4bad1a8 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/IAssociationStore.cs @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------- +// <copyright file="IAssociationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + /// <summary> + /// An enumeration that can specify how a given <see cref="Association"/> is used. + /// </summary> + public enum AssociationRelyingPartyType { + /// <summary> + /// The <see cref="Association"/> manages a shared secret between + /// Provider and Relying Party sites that allows the RP to verify + /// the signature on a message from an OP. + /// </summary> + Smart, + + /// <summary> + /// The <see cref="Association"/> manages a secret known alone by + /// a Provider that allows the Provider to verify its own signatures + /// for "dumb" (stateless) relying parties. + /// </summary> + Dumb + } + + /// <summary> + /// Stores <see cref="Association"/>s for lookup by their handle, keeping + /// associations separated by a given distinguishing factor (like which server the + /// association is with). + /// </summary> + /// <typeparam name="TKey"> + /// <see cref="System.Uri"/> for consumers (to distinguish associations across servers) or + /// <see cref="AssociationRelyingPartyType"/> for providers (to distinguish dumb and smart client associations). + /// </typeparam> + public interface IAssociationStore<TKey> { + /// <summary> + /// Saves an <see cref="Association"/> for later recall. + /// </summary> + /// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for providers).</param> + /// <param name="association">The association to store.</param> + void StoreAssociation(TKey distinguishingFactor, Association association); + + /// <summary> + /// Gets the best association (the one with the longest remaining life) for a given key. + /// </summary> + /// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <returns>The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.</returns> + Association GetAssociation(TKey distinguishingFactor); + + /// <summary> + /// Gets the association for a given key and handle. + /// </summary> + /// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <param name="handle">The handle of the specific association that must be recalled.</param> + /// <returns>The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.</returns> + Association GetAssociation(TKey distinguishingFactor, string handle); + + /// <summary>Removes a specified handle that may exist in the store.</summary> + /// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param> + /// <param name="handle">The handle of the specific association that must be deleted.</param> + /// <returns>True if the association existed in this store previous to this call.</returns> + /// <remarks> + /// No exception should be thrown if the association does not exist in the store + /// before this call. + /// </remarks> + bool RemoveAssociation(TKey distinguishingFactor, string handle); + + /// <summary> + /// Clears all expired associations from the store. + /// </summary> + /// <remarks> + /// If another algorithm is in place to periodically clear out expired associations, + /// this method call may be ignored. + /// This should be done frequently enough to avoid a memory leak, but sparingly enough + /// to not be a performance drain. + /// </remarks> + void ClearExpiredAssociations(); + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanRequest.cs index 474e9a7..3235d0d 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanRequest.cs @@ -7,39 +7,93 @@ namespace DotNetOpenAuth.OpenId.Messages { using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Reflection; + using Org.Mentalis.Security.Cryptography; /// <summary> /// An OpenID direct request from Relying Party to Provider to initiate an association that uses Diffie-Hellman encryption. /// </summary> internal class AssociateDiffieHellmanRequest : AssociateRequest { /// <summary> + /// The (only) value we use for the X variable in the Diffie-Hellman algorithm. + /// </summary> + internal static readonly int DefaultX = 1024; + + /// <summary> + /// The default gen value for the Diffie-Hellman algorithm. + /// </summary> + internal static readonly byte[] DefaultGen = { 2 }; + + /// <summary> + /// The default modulus value for the Diffie-Hellman algorithm. + /// </summary> + internal static readonly byte[] DefaultMod = { + 0, 220, 249, 58, 11, 136, 57, 114, 236, 14, 25, 152, 154, 197, 162, + 206, 49, 14, 29, 55, 113, 126, 141, 149, 113, 187, 118, 35, 115, 24, + 102, 230, 30, 247, 90, 46, 39, 137, 139, 5, 127, 152, 145, 194, 226, + 122, 99, 156, 63, 41, 182, 8, 20, 88, 28, 211, 178, 202, 57, 134, 210, + 104, 55, 5, 87, 125, 69, 194, 231, 229, 45, 200, 28, 122, 23, 24, 118, + 229, 206, 167, 75, 20, 72, 191, 223, 175, 24, 130, 142, 253, 37, 25, + 241, 78, 69, 227, 130, 102, 52, 175, 25, 73, 229, 181, 53, 204, 130, + 154, 72, 59, 138, 118, 34, 62, 93, 73, 10, 37, 127, 5, 189, 255, 22, + 242, 251, 34, 197, 131, 171 }; + + /// <summary> /// Initializes a new instance of the <see cref="AssociateDiffieHellmanRequest"/> class. /// </summary> /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> internal AssociateDiffieHellmanRequest(Uri providerEndpoint) : base(providerEndpoint) { + this.DiffieHellmanModulus = DefaultMod; + this.DiffieHellmanGen = DefaultGen; } /// <summary> /// Gets or sets the openid.dh_modulus value. /// </summary> + /// <value>May be null if the default value given in the OpenID spec is to be used.</value> [MessagePart("openid.dh_modulus", IsRequired = false, AllowEmpty = false)] internal byte[] DiffieHellmanModulus { get; set; } /// <summary> /// Gets or sets the openid.dh_gen value. /// </summary> + /// <value>May be null if the default value given in the OpenID spec is to be used.</value> [MessagePart("openid.dh_gen", IsRequired = false, AllowEmpty = false)] internal byte[] DiffieHellmanGen { get; set; } /// <summary> /// Gets or sets the openid.dh_consumer_public value. /// </summary> + /// <remarks> + /// This property is initialized with a call to <see cref="InitializeRequest"/>. + /// </remarks> [MessagePart("openid.dh_consumer_public", IsRequired = true, AllowEmpty = false)] internal byte[] DiffieHellmanConsumerPublic { get; set; } + + /// <summary> + /// Gets the Diffie-Hellman algorithm. + /// </summary> + /// <remarks> + /// This property is initialized with a call to <see cref="InitializeRequest"/>. + /// </remarks> + internal DiffieHellman Algorithm { get; private set; } + + /// <summary> + /// Called by the Relying Party to initialize the Diffie-Hellman algorithm and consumer public key properties. + /// </summary> + internal void InitializeRequest() { + if (this.DiffieHellmanModulus == null || this.DiffieHellmanGen == null) { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.DiffieHellmanRequiredPropertiesNotSet, string.Join(", ", new string[] { "DiffieHellmanModulus", "DiffieHellmanGen" }))); + } + + this.Algorithm = new DiffieHellmanManaged(this.DiffieHellmanModulus ?? DefaultMod, this.DiffieHellmanGen ?? DefaultGen, DefaultX); + byte[] consumerPublicKeyExchange = this.Algorithm.CreateKeyExchange(); + this.DiffieHellmanConsumerPublic = DiffieHellmanUtilities.EnsurePositive(consumerPublicKeyExchange); + } } } diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanResponse.cs index 666d170..7c3a07d 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanResponse.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanResponse.cs @@ -5,8 +5,11 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Security.Cryptography; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Reflection; + using Org.Mentalis.Security.Cryptography; /// <summary> /// The successful Diffie-Hellman association response message. @@ -16,17 +19,90 @@ namespace DotNetOpenAuth.OpenId.Messages { /// </remarks> internal class AssociateDiffieHellmanResponse : AssociateSuccessfulResponse { /// <summary> - /// Gets or sets the OP's Diffie-Hellman public key. + /// Gets or sets the Provider's Diffie-Hellman public key. /// </summary> /// <value>btwoc(g ^ xb mod p)</value> [MessagePart("dh_server_public", IsRequired = true, AllowEmpty = false)] - internal byte[] ServerPublic { get; set; } + internal byte[] DiffieHellmanServerPublic { get; set; } /// <summary> - /// Gets or sets the MAC key (shared secret), encrypted with the secret Diffie-Hellman value. H is either "SHA1" or "SHA256" depending on the session type. + /// Gets or sets the MAC key (shared secret), encrypted with the secret Diffie-Hellman value. /// </summary> - /// <value>H(btwoc(g ^ (xa * xb) mod p)) XOR MAC key</value> + /// <value>H(btwoc(g ^ (xa * xb) mod p)) XOR MAC key. H is either "SHA1" or "SHA256" depending on the session type. </value> [MessagePart("enc_mac_key", IsRequired = true, AllowEmpty = false)] internal byte[] EncodedMacKey { get; set; } + + /// <summary> + /// Called to create the Association based on a provided request previously given by the Relying Party. + /// </summary> + /// <param name="request">The request for an association.</param> + /// <returns>The created association.</returns> + /// <remarks> + /// <para>The response message is updated to include the details of the created association by this method, + /// but the resulting association is <i>not</i> added to the association store and must be done by the caller.</para> + /// <para>This method is called by both the Provider and the Relying Party, but actually performs + /// quite different operations in either scenario.</para> + /// </remarks> + internal Association CreateAssociation(AssociateDiffieHellmanRequest request) { + // If the encoded mac key is already set, then this is an incoming message at the Relying Party. + if (this.EncodedMacKey == null) { + return this.CreateAssociationAtProvider(request); + } else { + return this.CreateAssociationAtRelyingParty(request); + } + } + + /// <summary> + /// Creates the association at relying party side after the association response has been received. + /// </summary> + /// <param name="request">The original association request that was already sent and responded to.</param> + /// <returns>The newly created association.</returns> + /// <remarks> + /// The resulting association is <i>not</i> added to the association store and must be done by the caller. + /// </remarks> + private Association CreateAssociationAtRelyingParty(AssociateDiffieHellmanRequest request) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + HashAlgorithm hasher = DiffieHellmanUtilities.Lookup(Protocol, this.SessionType); + byte[] associationSecret = DiffieHellmanUtilities.SHAHashXorSecret(hasher, request.Algorithm, this.DiffieHellmanServerPublic, this.EncodedMacKey); + + Association association = HmacShaAssociation.Create(Protocol, this.AssociationType, this.AssociationHandle, associationSecret, TimeSpan.FromSeconds(this.ExpiresIn)); + return association; + } + + /// <summary> + /// Creates the association at the provider side after the association request has been received. + /// </summary> + /// <param name="request">The association request.</param> + /// <returns>The newly created association.</returns> + /// <remarks> + /// The response message is updated to include the details of the created association by this method, + /// but the resulting association is <i>not</i> added to the association store and must be done by the caller. + /// </remarks> + private Association CreateAssociationAtProvider(AssociateDiffieHellmanRequest request) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + this.SessionType = this.SessionType ?? request.SessionType; + + // Go ahead and create the association first, complete with its secret that we're about to share. + Association association = HmacShaAssociation.Create(this.Protocol, this.AssociationType, AssociationRelyingPartyType.Smart); + this.AssociationHandle = association.Handle; + this.ExpiresIn = association.SecondsTillExpiration; + + // We now need to securely communicate the secret to the relying party using Diffie-Hellman. + // We do this by performing a DH algorithm on the secret and setting a couple of properties + // that will be transmitted to the Relying Party. The RP will perform an inverse operation + // using its part of a DH secret in order to decrypt the shared secret we just invented + // above when we created the association. + DiffieHellman dh = new DiffieHellmanManaged( + request.DiffieHellmanModulus ?? AssociateDiffieHellmanRequest.DefaultMod, + request.DiffieHellmanGen ?? AssociateDiffieHellmanRequest.DefaultGen, + AssociateDiffieHellmanRequest.DefaultX); + HashAlgorithm hasher = DiffieHellmanUtilities.Lookup(this.Protocol, this.SessionType); + this.DiffieHellmanServerPublic = DiffieHellmanUtilities.EnsurePositive(dh.CreateKeyExchange()); + this.EncodedMacKey = DiffieHellmanUtilities.SHAHashXorSecret(hasher, dh, request.DiffieHellmanConsumerPublic, association.SecretKey); + + return association; + } } } diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs index 23eb0f9..c5adb7e 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs @@ -16,12 +16,12 @@ namespace DotNetOpenAuth.OpenId.Messages { /// An OpenID direct request from Relying Party to Provider to initiate an association. /// </summary> [DebuggerDisplay("OpenID {ProtocolVersion} {Mode} {AssociationType} {SessionType}")] - internal class AssociateRequest : RequestBase { + internal abstract class AssociateRequest : RequestBase { /// <summary> /// Initializes a new instance of the <see cref="AssociateRequest"/> class. /// </summary> /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> - internal AssociateRequest(Uri providerEndpoint) + protected AssociateRequest(Uri providerEndpoint) : base(providerEndpoint, "associate", MessageTransport.Direct) { } @@ -56,7 +56,7 @@ namespace DotNetOpenAuth.OpenId.Messages { base.EnsureValidMessage(); ErrorUtilities.Verify( - this.SessionType != "no-encryption" || this.Recipient.IsTransportSecure(), + !string.Equals(this.SessionType, Protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal) || this.Recipient.IsTransportSecure(), OpenIdStrings.NoEncryptionSessionRequiresHttps, this); } diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedRequest.cs new file mode 100644 index 0000000..836c893 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedRequest.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateUnencryptedRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// Represents an association request that is sent using HTTPS and otherwise communicates the shared secret in plain text. + /// </summary> + internal class AssociateUnencryptedRequest : AssociateRequest { + /// <summary> + /// Initializes a new instance of the <see cref="AssociateUnencryptedRequest"/> class. + /// </summary> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + internal AssociateUnencryptedRequest(Uri providerEndpoint) + : base(providerEndpoint) { + SessionType = Protocol.Args.SessionType.NoEncryption; + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + ErrorUtilities.Verify( + string.Equals(SessionType, Protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal), + MessagingStrings.UnexpectedMessagePartValueForConstant, + GetType().Name, + "openid.session_type", + Protocol.Args.SessionType.NoEncryption, + SessionType); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedResponse.cs index db365a6..4d0a341 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedResponse.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedResponse.cs @@ -16,6 +16,13 @@ namespace DotNetOpenAuth.OpenId.Messages { /// </remarks> internal class AssociateUnencryptedResponse : AssociateSuccessfulResponse { /// <summary> + /// Initializes a new instance of the <see cref="AssociateUnencryptedResponse"/> class. + /// </summary> + internal AssociateUnencryptedResponse() { + SessionType = Protocol.Args.SessionType.NoEncryption; + } + + /// <summary> /// Gets or sets the MAC key (shared secret) for this association, Base 64 (Josefsson, S., “The Base16, Base32, and Base64 Data Encodings,” .) [RFC3548] encoded. /// </summary> [MessagePart("mac_key", IsRequired = true, AllowEmpty = false)] diff --git a/src/DotNetOpenAuth/OpenId/Messages/DirectResponseBase.cs b/src/DotNetOpenAuth/OpenId/Messages/DirectResponseBase.cs index b74288d..7a7ded8 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/DirectResponseBase.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/DirectResponseBase.cs @@ -36,7 +36,7 @@ namespace DotNetOpenAuth.OpenId.Messages { protected DirectResponseBase() { } - #region IProtocolMessage Members + #region IProtocolMessage Properties /// <summary> /// Gets the version of the protocol this message is prepared to implement. @@ -70,6 +70,17 @@ namespace DotNetOpenAuth.OpenId.Messages { get { return EmptyDictionary<string, string>.Instance; } } + #endregion + + /// <summary> + /// Gets the protocol used by this message. + /// </summary> + protected Protocol Protocol { + get { return Protocol.Lookup(this.ProtocolVersion); } + } + + #region IProtocolMessage methods + /// <summary> /// Checks the message state for conformity to the protocol specification /// and throws an exception if the message is invalid. diff --git a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs index 861e28f..a8bc18b 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs @@ -82,7 +82,7 @@ namespace DotNetOpenAuth.OpenId.Messages { #endregion - #region IProtocolMessage Members + #region IProtocolMessage Properties /// <summary> /// Gets the version of the protocol this message is prepared to implement. @@ -114,6 +114,17 @@ namespace DotNetOpenAuth.OpenId.Messages { get { return EmptyDictionary<string, string>.Instance; } } + #endregion + + /// <summary> + /// Gets the protocol used by this message. + /// </summary> + protected Protocol Protocol { + get { return Protocol.Lookup(this.ProtocolVersion); } + } + + #region IProtocolMessage Methods + /// <summary> /// Checks the message state for conformity to the protocol specification /// and throws an exception if the message is invalid. diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index e6ff018..3555c2e 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -61,6 +61,24 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.. + /// </summary> + internal static string BadAssociationPrivateData { + get { + return ResourceManager.GetString("BadAssociationPrivateData", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}. + /// </summary> + internal static string DiffieHellmanRequiredPropertiesNotSet { + get { + return ResourceManager.GetString("DiffieHellmanRequiredPropertiesNotSet", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}'). /// </summary> internal static string InvalidCharacterInKeyValueFormInput { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index 00d6d1b..0ba7fb3 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -117,6 +117,12 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> + <data name="BadAssociationPrivateData" xml:space="preserve"> + <value>The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.</value> + </data> + <data name="DiffieHellmanRequiredPropertiesNotSet" xml:space="preserve"> + <value>The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}</value> + </data> <data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve"> <value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}')</value> </data> diff --git a/src/DotNetOpenAuth/OpenId/Protocol.cs b/src/DotNetOpenAuth/OpenId/Protocol.cs index 551c05a..4106ec6 100644 --- a/src/DotNetOpenAuth/OpenId/Protocol.cs +++ b/src/DotNetOpenAuth/OpenId/Protocol.cs @@ -1,17 +1,414 @@ -//----------------------------------------------------------------------- +// <auto-generated/> // disable StyleCop on this file +//----------------------------------------------------------------------- // <copyright file="Protocol.cs" company="Andrew Arnott"> // Copyright (c) Andrew Arnott. All rights reserved. // </copyright> //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using System.Globalization; + + /// <summary> + /// An enumeration of the OpenID protocol versions supported by this library. + /// </summary> + public enum ProtocolVersion { + /// <summary> + /// OpenID Authentication 1.0 + /// </summary> + V10, + /// <summary> + /// OpenID Authentication 1.1 + /// </summary> + V11, + /// <summary> + /// OpenID Authentication 2.0 + /// </summary> + V20, + } + /// <summary> - /// OpenID Protocol constants + /// Tracks the several versions of OpenID this library supports and the unique + /// constants to each version used in the protocol. /// </summary> internal class Protocol { /// <summary> /// The value of the openid.ns parameter in the OpenID 2.0 specification. /// </summary> internal const string OpenId2Namespace = "http://specs.openid.net/auth/2.0"; + + /// <summary> + /// Scans a list for matches with some element of the OpenID protocol, + /// searching from newest to oldest protocol for the first and best match. + /// </summary> + /// <typeparam name="T">The type of element retrieved from the <see cref="Protocol"/> instance.</typeparam> + /// <param name="elementOf">Takes a <see cref="Protocol"/> instance and returns an element of it.</param> + /// <param name="list">The list to scan for matches.</param> + /// <returns>The protocol with the element that matches some item in the list.</returns> + internal static Protocol FindBestVersion<T>(Func<Protocol, T> elementOf, IEnumerable<T> list) { + foreach (var protocol in Protocol.AllVersions) { + foreach (var item in list) { + if (item != null && item.Equals(elementOf(protocol))) + return protocol; + } + } + return null; + } + + Protocol(QueryParameters queryBits) { + openidnp = queryBits; + openid = new QueryParameters(queryBits); + } + + // Well-known, supported versions of the OpenID spec. + public static readonly Protocol V10 = new Protocol(new QueryParameters()) { + Version = new Version(1, 0), + XmlNamespace = "http://openid.net/xmlns/1.0", + QueryDeclaredNamespaceVersion = null, + ClaimedIdentifierServiceTypeURI = "http://openid.net/signon/1.0", + OPIdentifierServiceTypeURI = null, // not supported + ClaimedIdentifierForOPIdentifier = null, // not supported + RPReturnToTypeURI = null, // not supported + HtmlDiscoveryProviderKey = "openid.server", + HtmlDiscoveryLocalIdKey = "openid.delegate", + }; + public static readonly Protocol V11 = new Protocol(new QueryParameters()) { + Version = new Version(1, 1), + XmlNamespace = "http://openid.net/xmlns/1.0", + QueryDeclaredNamespaceVersion = null, + ClaimedIdentifierServiceTypeURI = "http://openid.net/signon/1.1", + OPIdentifierServiceTypeURI = null, // not supported + ClaimedIdentifierForOPIdentifier = null, // not supported + RPReturnToTypeURI = null, // not supported + HtmlDiscoveryProviderKey = "openid.server", + HtmlDiscoveryLocalIdKey = "openid.delegate", + }; + public static readonly Protocol V20 = new Protocol(new QueryParameters() { + Realm = "realm", + op_endpoint = "op_endpoint", + response_nonce = "response_nonce", + error_code = "error_code", + user_setup_url = null, + }) { + Version = new Version(2, 0), + XmlNamespace = null, // no longer applicable + QueryDeclaredNamespaceVersion = Protocol.OpenId2Namespace, + ClaimedIdentifierServiceTypeURI = "http://specs.openid.net/auth/2.0/signon", + OPIdentifierServiceTypeURI = "http://specs.openid.net/auth/2.0/server", + ClaimedIdentifierForOPIdentifier = "http://specs.openid.net/auth/2.0/identifier_select", + RPReturnToTypeURI = "http://specs.openid.net/auth/2.0/return_to", + HtmlDiscoveryProviderKey = "openid2.provider", + HtmlDiscoveryLocalIdKey = "openid2.local_id", + Args = new QueryArguments() { + SessionType = new QueryArguments.SessionTypes() { + NoEncryption = "no-encryption", + DH_SHA256 = "DH-SHA256", + DH_SHA384 = "DH-SHA384", + DH_SHA512 = "DH-SHA512", + }, + SignatureAlgorithm = new QueryArguments.SignatureAlgorithms() { + HMAC_SHA256 = "HMAC-SHA256", + HMAC_SHA384 = "HMAC-SHA384", + HMAC_SHA512 = "HMAC-SHA512", + }, + Mode = new QueryArguments.Modes() { + setup_needed = "setup_needed", + }, + }, + }; + /// <summary> + /// A list of all supported OpenID versions, in order starting from newest version. + /// </summary> + public readonly static List<Protocol> AllVersions = new List<Protocol>() { V20, V11, V10 }; + /// <summary> + /// The default (or most recent) supported version of the OpenID protocol. + /// </summary> + public readonly static Protocol Default = AllVersions[0]; + public static Protocol Lookup(Version version) { + foreach (Protocol protocol in AllVersions) { + if (protocol.Version == version) return protocol; + } + throw new ArgumentOutOfRangeException("version"); + } + public static Protocol Lookup(ProtocolVersion version) { + switch (version) { + case ProtocolVersion.V10: return Protocol.V10; + case ProtocolVersion.V11: return Protocol.V11; + case ProtocolVersion.V20: return Protocol.V20; + default: throw new ArgumentOutOfRangeException("version"); + } + } + /// <summary> + /// Attempts to detect the right OpenID protocol version based on the contents + /// of an incoming OpenID <i>indirect</i> message or <i>direct request</i>. + /// </summary> + internal static Protocol Detect(IDictionary<string, string> query) { + if (query == null) throw new ArgumentNullException("query"); + return query.ContainsKey(V20.openid.ns) ? V20 : V11; + } + /// <summary> + /// Attempts to detect the right OpenID protocol version based on the contents + /// of an incoming OpenID <i>direct</i> response message. + /// </summary> + internal static Protocol DetectFromDirectResponse(IDictionary<string, string> query) { + if (query == null) throw new ArgumentNullException("query"); + return query.ContainsKey(V20.openidnp.ns) ? V20 : V11; + } + /// <summary> + /// Attemps to detect the highest OpenID protocol version supported given a set + /// of XRDS Service Type URIs included for some service. + /// </summary> + internal static Protocol Detect(string[] serviceTypeURIs) { + if (serviceTypeURIs == null) throw new ArgumentNullException("serviceTypeURIs"); + return FindBestVersion(p => p.OPIdentifierServiceTypeURI, serviceTypeURIs) ?? + FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, serviceTypeURIs) ?? + FindBestVersion(p => p.RPReturnToTypeURI, serviceTypeURIs); + } + + /// <summary> + /// The OpenID version that this <see cref="Protocol"/> instance describes. + /// </summary> + public Version Version; + /// <summary> + /// Returns the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance. + /// </summary> + public ProtocolVersion ProtocolVersion { + get { + switch (Version.Major) { + case 1: return ProtocolVersion.V11; + case 2: return ProtocolVersion.V20; + default: throw new ArgumentException(null); // this should never happen + } + } + } + /// <summary> + /// The namespace of OpenId 1.x elements in XRDS documents. + /// </summary> + public string XmlNamespace; + /// <summary> + /// The value of the openid.ns parameter that appears on the query string + /// whenever data is passed between relying party and provider for OpenID 2.0 + /// and later. + /// </summary> + public string QueryDeclaredNamespaceVersion; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on a Claimed Identifier (http://andrewarnott.yahoo.com) + /// </summary> + public string ClaimedIdentifierServiceTypeURI; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on an OP Identifier rather than a Claimed Identifier. + /// (http://yahoo.com) + /// </summary> + public string OPIdentifierServiceTypeURI; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on a Realm URL and looking for the endpoint URL + /// that can receive authentication assertions. + /// </summary> + public string RPReturnToTypeURI; + /// <summary> + /// Used as the Claimed Identifier and the OP Local Identifier when + /// the User Supplied Identifier is an OP Identifier. + /// </summary> + public string ClaimedIdentifierForOPIdentifier; + /// <summary> + /// The value of the 'rel' attribute in an HTML document's LINK tag + /// when the same LINK tag's HREF attribute value contains the URL to an + /// OP Endpoint URL. + /// </summary> + public string HtmlDiscoveryProviderKey; + /// <summary> + /// The value of the 'rel' attribute in an HTML document's LINK tag + /// when the same LINK tag's HREF attribute value contains the URL to use + /// as the OP Local Identifier. + /// </summary> + public string HtmlDiscoveryLocalIdKey; + /// <summary> + /// Parts of the protocol that define parameter names that appear in the + /// query string. Each parameter name is prefixed with 'openid.'. + /// </summary> + public readonly QueryParameters openid; + /// <summary> + /// Parts of the protocol that define parameter names that appear in the + /// query string. Each parameter name is NOT prefixed with 'openid.'. + /// </summary> + public readonly QueryParameters openidnp; + /// <summary> + /// The various 'constants' that appear as parameter arguments (values). + /// </summary> + public QueryArguments Args = new QueryArguments(); + + internal class QueryParameters { + /// <summary> + /// The value "openid." + /// </summary> + public string Prefix = "openid."; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")] + public QueryParameters() { } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")] + public QueryParameters(QueryParameters addPrefixTo) { + ns = addPrefix(addPrefixTo.ns); + return_to = addPrefix(addPrefixTo.return_to); + Realm = addPrefix(addPrefixTo.Realm); + mode = addPrefix(addPrefixTo.mode); + error = addPrefix(addPrefixTo.error); + error_code = addPrefix(addPrefixTo.error_code); + identity = addPrefix(addPrefixTo.identity); + op_endpoint = addPrefix(addPrefixTo.op_endpoint); + response_nonce = addPrefix(addPrefixTo.response_nonce); + claimed_id = addPrefix(addPrefixTo.claimed_id); + expires_in = addPrefix(addPrefixTo.expires_in); + assoc_type = addPrefix(addPrefixTo.assoc_type); + assoc_handle = addPrefix(addPrefixTo.assoc_handle); + session_type = addPrefix(addPrefixTo.session_type); + is_valid = addPrefix(addPrefixTo.is_valid); + sig = addPrefix(addPrefixTo.sig); + signed = addPrefix(addPrefixTo.signed); + user_setup_url = addPrefix(addPrefixTo.user_setup_url); + invalidate_handle = addPrefix(addPrefixTo.invalidate_handle); + dh_modulus = addPrefix(addPrefixTo.dh_modulus); + dh_gen = addPrefix(addPrefixTo.dh_gen); + dh_consumer_public = addPrefix(addPrefixTo.dh_consumer_public); + dh_server_public = addPrefix(addPrefixTo.dh_server_public); + enc_mac_key = addPrefix(addPrefixTo.enc_mac_key); + mac_key = addPrefix(addPrefixTo.mac_key); + } + string addPrefix(string original) { + return (original != null) ? Prefix + original : null; + } + // These fields default to 1.x specifications, and are overridden + // as necessary by later versions in the Protocol class initializers. + // Null values in any version suggests that that feature is absent from that version. + public string ns = "ns"; + public string return_to = "return_to"; + public string Realm = "trust_root"; + public string mode = "mode"; + public string error = "error"; + public string error_code = null; + public string identity = "identity"; + public string op_endpoint = null; + public string response_nonce = null; + public string claimed_id = "claimed_id"; + public string expires_in = "expires_in"; + public string assoc_type = "assoc_type"; + public string assoc_handle = "assoc_handle"; + public string session_type = "session_type"; + public string is_valid = "is_valid"; + public string sig = "sig"; + public string signed = "signed"; + public string user_setup_url = "user_setup_url"; + public string invalidate_handle = "invalidate_handle"; + public string dh_modulus = "dh_modulus"; + public string dh_gen = "dh_gen"; + public string dh_consumer_public = "dh_consumer_public"; + public string dh_server_public = "dh_server_public"; + public string enc_mac_key = "enc_mac_key"; + public string mac_key = "mac_key"; + } + internal class QueryArguments { + public ErrorCodes ErrorCode = new ErrorCodes(); + public SessionTypes SessionType = new SessionTypes(); + public SignatureAlgorithms SignatureAlgorithm = new SignatureAlgorithms(); + public Modes Mode = new Modes(); + public IsValidValues IsValid = new IsValidValues(); + + internal class ErrorCodes { + public string UnsupportedType = "unsupported-type"; + } + internal class SessionTypes { + /// <summary> + /// A preference order list of all supported session types. + /// </summary> + public string[] All { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1, NoEncryption }; } } + public string[] AllDiffieHellman { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1 }; } } + public string DH_SHA1 = "DH-SHA1"; + public string DH_SHA256 = null; + public string DH_SHA384 = null; + public string DH_SHA512 = null; + public string NoEncryption = string.Empty; + public string Best { + get { + foreach (string algorithmName in All) { + if (algorithmName != null) { + return algorithmName; + } + } + throw new ProtocolException(); // really bad... we have no signing algorithms at all + } + } + } + internal class SignatureAlgorithms { + /// <summary> + /// A preference order list of signature algorithms we support. + /// </summary> + public string[] All { get { return new[] { HMAC_SHA512, HMAC_SHA384, HMAC_SHA256, HMAC_SHA1 }; } } + public string HMAC_SHA1 = "HMAC-SHA1"; + public string HMAC_SHA256 = null; + public string HMAC_SHA384 = null; + public string HMAC_SHA512 = null; + public string Best { + get { + foreach (string algorithmName in All) { + if (algorithmName != null) { + return algorithmName; + } + } + throw new ProtocolException(); // really bad... we have no signing algorithms at all + } + } + } + internal class Modes { + public string cancel = "cancel"; + public string error = "error"; + public string id_res = "id_res"; + public string checkid_immediate = "checkid_immediate"; + public string checkid_setup = "checkid_setup"; + public string check_authentication = "check_authentication"; + public string associate = "associate"; + public string setup_needed = null; + } + internal class IsValidValues { + public string True = "true"; + public string False = "false"; + } + } + + /// <summary> + /// The maximum time a user can be allowed to take to complete authentication. + /// </summary> + /// <remarks> + /// This is used to calculate the length of time that nonces are stored. + /// This is internal until we can decide whether to leave this static, or make + /// it an instance member, or put it inside the IConsumerApplicationStore interface. + /// </remarks> + internal static TimeSpan MaximumUserAgentAuthenticationTime = TimeSpan.FromMinutes(5); + /// <summary> + /// The maximum permissible difference in clocks between relying party and + /// provider web servers, discounting time zone differences. + /// </summary> + /// <remarks> + /// This is used when storing/validating nonces from the provider. + /// If it is conceivable that a server's clock could be up to five minutes + /// off from true UTC time, then the maximum time skew should be set to + /// ten minutes to allow one server to be five minutes ahead and the remote + /// server to be five minutes behind and still be able to communicate. + /// </remarks> + internal static TimeSpan MaximumAllowableTimeSkew = TimeSpan.FromMinutes(10); + + public override bool Equals(object obj) { + Protocol other = obj as Protocol; + if (other == null) return false; + return this.Version == other.Version; + } + public override int GetHashCode() { + return Version.GetHashCode(); + } + public override string ToString() { + return string.Format(CultureInfo.CurrentCulture, "OpenID Authentication {0}.{1}", Version.Major, Version.Minor); + } } } diff --git a/src/DotNetOpenAuth/Util.cs b/src/DotNetOpenAuth/Util.cs index b54ada9..a4fdb9d 100644 --- a/src/DotNetOpenAuth/Util.cs +++ b/src/DotNetOpenAuth/Util.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth { using System; using System.Collections.Generic; using System.Globalization; + using System.Linq; using System.Reflection; /// <summary> |