summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2008-11-11 20:27:50 -0800
committerAndrew <andrewarnott@gmail.com>2008-11-11 20:27:50 -0800
commit3e75e9c22a07a079103b53fc38a985ab66384d0d (patch)
tree2262fbdca4a861129743e9f7784cea061bf22b06
parente8ab62f93adad1205fa572285a403e8b91e2ccf5 (diff)
downloadDotNetOpenAuth-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.
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/ChannelElements/KeyValueFormEncodingTests.cs2
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs4
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/Messages/AssociateRequestTests.cs33
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/ScenarioTests.cs30
-rw-r--r--src/DotNetOpenAuth.Test/TestUtilities.cs28
-rw-r--r--src/DotNetOpenAuth.sln4
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj8
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs9
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingStrings.resx3
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs55
-rw-r--r--src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs7
-rw-r--r--src/DotNetOpenAuth/OAuth/ChannelElements/StandardTokenGenerator.cs8
-rw-r--r--src/DotNetOpenAuth/OpenId/Association.cs280
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs36
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/KeyValueFormEncoding.cs58
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/SigningBindingElement.cs151
-rw-r--r--src/DotNetOpenAuth/OpenId/Configuration.cs34
-rw-r--r--src/DotNetOpenAuth/OpenId/DiffieHellmanUtilities.cs128
-rw-r--r--src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs195
-rw-r--r--src/DotNetOpenAuth/OpenId/IAssociationStore.cs80
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanRequest.cs54
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/AssociateDiffieHellmanResponse.cs84
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs6
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedRequest.cs49
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/AssociateUnencryptedResponse.cs7
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/DirectResponseBase.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs18
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.resx6
-rw-r--r--src/DotNetOpenAuth/OpenId/Protocol.cs401
-rw-r--r--src/DotNetOpenAuth/Util.cs1
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&amp;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&lt;TKey&gt;.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&lt;TKey, TValue&gt; 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 &apos;{0}&apos; because it contains an illegal character for Key-Value Form encoding. (line {1}: &apos;{2}&apos;).
/// </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>