summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2011-05-20 08:26:54 -0700
committerAndrew Arnott <andrewarnott@gmail.com>2011-05-20 08:26:54 -0700
commit9aa1e046c6ccd6fa0859709419c091bf6b52f465 (patch)
tree739c8704e96cec4ee1eb9846e854d32478b856f5 /src
parent9ba3d8e9f066132c68501fbc191acd50fba905f4 (diff)
downloadDotNetOpenAuth-9aa1e046c6ccd6fa0859709419c091bf6b52f465.zip
DotNetOpenAuth-9aa1e046c6ccd6fa0859709419c091bf6b52f465.tar.gz
DotNetOpenAuth-9aa1e046c6ccd6fa0859709419c091bf6b52f465.tar.bz2
Providers and Relying Parties both implement a unified pair of ICryptoKeyStore and INonceStore.
OPs can configure to use encoded association handles or database-backed ones based on a simple web.config switch.
Diffstat (limited to 'src')
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/AuthenticationTests.cs5
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs4
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionTestUtilities.cs5
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd9
-rw-r--r--src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs14
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj1
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs49
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs24
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/IProviderApplicationStore.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/IProviderAssociationStore.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs61
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs62
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationKeyStorage.cs45
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/ProviderSecuritySettings.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs25
15 files changed, 253 insertions, 68 deletions
diff --git a/src/DotNetOpenAuth.Test/OpenId/AuthenticationTests.cs b/src/DotNetOpenAuth.Test/OpenId/AuthenticationTests.cs
index 63e9fdc..b1548c1 100644
--- a/src/DotNetOpenAuth.Test/OpenId/AuthenticationTests.cs
+++ b/src/DotNetOpenAuth.Test/OpenId/AuthenticationTests.cs
@@ -139,7 +139,8 @@ namespace DotNetOpenAuth.Test.OpenId {
Contract.Requires<ArgumentException>(!statelessRP || !sharedAssociation, "The RP cannot be stateless while sharing an association with the OP.");
Contract.Requires<ArgumentException>(positive || !tamper, "Cannot tamper with a negative response.");
var securitySettings = new ProviderSecuritySettings();
- var associationStore = new ProviderAssociationHandleEncoder();
+ var cryptoKeyStore = new MemoryCryptoKeyStore();
+ var associationStore = new ProviderAssociationHandleEncoder(cryptoKeyStore);
Association association = sharedAssociation ? HmacShaAssociation.Create(protocol, protocol.Args.SignatureAlgorithm.Best, AssociationRelyingPartyType.Smart, associationStore, securitySettings) : null;
var coordinator = new OpenIdCoordinator(
rp => {
@@ -197,7 +198,7 @@ namespace DotNetOpenAuth.Test.OpenId {
}
},
op => {
- ((ProviderAssociationHandleEncoder)op.AssociationStore).Secret = associationStore.Secret;
+ op.CryptoKeyStore.StoreKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, association.Handle, cryptoKeyStore.GetKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, association.Handle));
var request = op.Channel.ReadFromRequest<CheckIdRequest>();
Assert.IsNotNull(request);
IProtocolMessage response;
diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs
index 56b6b9a..9d0ee1f 100644
--- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs
+++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs
@@ -23,7 +23,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements {
public void SignaturesMatchKnownGood() {
Protocol protocol = Protocol.V20;
var settings = new ProviderSecuritySettings();
- var store = new ProviderAssociationHandleEncoder();
+ var store = new ProviderAssociationHandleEncoder(new MemoryCryptoKeyStore());
byte[] associationSecret = Convert.FromBase64String("rsSwv1zPWfjPRQU80hciu8FPDC+GONAMJQ/AvSo1a2M=");
string handle = store.Serialize(associationSecret, DateTime.UtcNow.AddDays(1), false);
Association association = HmacShaAssociation.Create(handle, associationSecret, TimeSpan.FromDays(1));
@@ -45,7 +45,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements {
[TestCase]
public void SignedResponsesIncludeExtraDataInSignature() {
Protocol protocol = Protocol.Default;
- SigningBindingElement sbe = new SigningBindingElement(new ProviderAssociationHandleEncoder(), new ProviderSecuritySettings());
+ SigningBindingElement sbe = new SigningBindingElement(new ProviderAssociationHandleEncoder(new MemoryCryptoKeyStore()), new ProviderSecuritySettings());
sbe.Channel = new TestChannel(this.MessageDescriptions);
IndirectSignedResponse response = new IndirectSignedResponse(protocol.Version, RPUri);
response.ReturnTo = RPUri;
diff --git a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionTestUtilities.cs b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionTestUtilities.cs
index 4ca2c90..32c7cdf 100644
--- a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionTestUtilities.cs
+++ b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionTestUtilities.cs
@@ -34,7 +34,8 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions {
IEnumerable<IOpenIdMessageExtension> requests,
IEnumerable<IOpenIdMessageExtension> responses) {
var securitySettings = new ProviderSecuritySettings();
- var associationStore = new ProviderAssociationHandleEncoder();
+ var cryptoKeyStore = new MemoryCryptoKeyStore();
+ var associationStore = new ProviderAssociationHandleEncoder(cryptoKeyStore);
Association association = HmacShaAssociation.Create(protocol, protocol.Args.SignatureAlgorithm.Best, AssociationRelyingPartyType.Smart, associationStore, securitySettings);
var coordinator = new OpenIdCoordinator(
rp => {
@@ -58,7 +59,7 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions {
},
op => {
RegisterExtension(op.Channel, Mocks.MockOpenIdExtension.Factory);
- ((ProviderAssociationHandleEncoder)op.AssociationStore).Secret = associationStore.Secret;
+ op.CryptoKeyStore.StoreKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, association.Handle, cryptoKeyStore.GetKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, association.Handle));
var request = op.Channel.ReadFromRequest<CheckIdRequest>();
var response = new PositiveAssertionResponse(request);
var receivedRequests = request.Extensions.Cast<IOpenIdMessageExtension>();
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
index 689ad2e..57433e7 100644
--- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
+++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
@@ -581,6 +581,15 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
+ <xs:attribute name="encodeAssociationSecretsInHandles" type="xs:boolean" default="true">
+ <xs:annotation>
+ <xs:documentation>
+ Whether the Provider should ease the burden of storing associations
+ by encoding their secrets (in signed, encrypted form) into the association handles themselves, storing only
+ a few rotating, private symmetric keys in the Provider's store instead.
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
<xs:attribute name="unsolicitedAssertionVerification">
<xs:annotation>
<xs:documentation>
diff --git a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
index 3545fc5..8a922f7 100644
--- a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
+++ b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
@@ -35,6 +35,8 @@ namespace DotNetOpenAuth.Configuration {
/// </summary>
private const string AssociationsConfigName = "associations";
+ private const string EncodeAssociationSecretsInHandlesConfigName = "encodeAssociationSecretsInHandles";
+
/// <summary>
/// Gets the name of the @requireSsl attribute.
/// </summary>
@@ -116,6 +118,17 @@ namespace DotNetOpenAuth.Configuration {
}
/// <summary>
+ /// Gets or sets a value indicating whether the Provider should ease the burden of storing associations
+ /// by encoding their secrets (in signed, encrypted form) into the association handles themselves, storing only
+ /// a few rotating, private symmetric keys in the Provider's store instead.
+ /// </summary>
+ [ConfigurationProperty(EncodeAssociationSecretsInHandlesConfigName, DefaultValue = ProviderSecuritySettings.EncodeAssociationSecretsInHandlesDefault)]
+ public bool EncodeAssociationSecretsInHandles {
+ get { return (bool)this[EncodeAssociationSecretsInHandlesConfigName]; }
+ set { this[EncodeAssociationSecretsInHandlesConfigName] = value; }
+ }
+
+ /// <summary>
/// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file.
/// </summary>
/// <returns>The newly created security settings object.</returns>
@@ -126,6 +139,7 @@ namespace DotNetOpenAuth.Configuration {
settings.MaximumHashBitLength = this.MaximumHashBitLength;
settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks;
settings.UnsolicitedAssertionVerification = this.UnsolicitedAssertionVerification;
+ settings.EncodeAssociationSecretsInHandles = this.EncodeAssociationSecretsInHandles;
foreach (AssociationTypeElement element in this.AssociationLifetimes) {
Contract.Assume(element != null);
settings.AssociationLifetimes.Add(element.AssociationType, element.MaximumLifetime);
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index c91f92b..8ba8b0c 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -503,6 +503,7 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="OpenId\Provider\AssociationDataBag.cs" />
<Compile Include="OpenId\Provider\IProviderAssociationStore.cs" />
<Compile Include="OpenId\Provider\ProviderAssociationHandleEncoder.cs" />
+ <Compile Include="OpenId\Provider\ProviderAssociationKeyStorage.cs" />
<Compile Include="OpenId\RelyingParty\CryptoKeyStoreAsRelyingPartyAssociationStore.cs" />
<Compile Include="OpenId\RelyingParty\IRelyingPartyAssociationStore.cs" />
<Compile Include="OpenId\RelyingParty\Associations.cs" />
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index 3062f23..4bd3895 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -66,6 +66,17 @@ namespace DotNetOpenAuth.Messaging {
internal const string AlphaNumeric = UppercaseLetters + LowercaseLetters + Digits;
/// <summary>
+ /// All the characters that are allowed for use as a base64 encoding character.
+ /// </summary>
+ internal const string Base64Characters = AlphaNumeric + "+" + "/";
+
+ /// <summary>
+ /// All the characters that are allowed for use as a base64 encoding character
+ /// in the "web safe" context.
+ /// </summary>
+ internal const string Base64WebSafeCharacters = AlphaNumeric + "-" + "_";
+
+ /// <summary>
/// The set of digits, and alphabetic letters (upper and lowercase) that are clearly
/// visually distinguishable.
/// </summary>
@@ -692,7 +703,7 @@ namespace DotNetOpenAuth.Messaging {
int failedAttempts = 0;
tryAgain:
try {
- string handle = GetRandomString(SymmetricSecretHandleLength, AlphaNumeric);
+ string handle = GetRandomString(SymmetricSecretHandleLength, Base64WebSafeCharacters);
cryptoKeyPair = new KeyValuePair<string, CryptoKey>(handle, cryptoKey);
cryptoKeyStore.StoreKey(bucket, handle, cryptoKey);
} catch (CryptoKeyCollisionException) {
@@ -734,6 +745,42 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Converts to data buffer to a base64-encoded string, using web safe characters and with the padding removed.
+ /// </summary>
+ /// <param name="data">The data buffer.</param>
+ /// <returns>A web-safe base64-encoded string without padding.</returns>
+ internal static string ConvertToBase64WebSafeString(byte[] data) {
+ var builder = new StringBuilder(Convert.ToBase64String(data));
+
+ // Swap out the URL-unsafe characters, and trim the padding characters.
+ builder.Replace('+', '-').Replace('/', '_');
+ while (builder[builder.Length - 1] == '=') { // should happen at most twice.
+ builder.Length -= 1;
+ }
+
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Decodes a (web-safe) base64-string back to its binary buffer form.
+ /// </summary>
+ /// <param name="base64WebSafe">The base64-encoded string. May be web-safe encoded.</param>
+ /// <returns>A data buffer.</returns>
+ internal static byte[] FromBase64WebSafeString(string base64WebSafe) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(base64WebSafe));
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+
+ // Restore the padding characters and original URL-unsafe characters.
+ int missingPaddingCharacters = 4 - (base64WebSafe.Length % 4);
+ ErrorUtilities.VerifyInternal(missingPaddingCharacters <= 2, "No more than two padding characters should be present for base64.");
+ var builder = new StringBuilder(base64WebSafe, base64WebSafe.Length + missingPaddingCharacters);
+ builder.Replace('-', '+').Replace('_', '/');
+ builder.Append('=', missingPaddingCharacters);
+
+ return Convert.FromBase64String(builder.ToString());
+ }
+
+ /// <summary>
/// Compares to string values for ordinal equality in such a way that its execution time does not depend on how much of the value matches.
/// </summary>
/// <param name="value1">The first value.</param>
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
index 02a1c00..fc37954 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
@@ -58,12 +58,12 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// Initializes a new instance of the <see cref="OpenIdChannel"/> class
/// for use by a Provider.
/// </summary>
- /// <param name="associationStore">The OpenID Provider's association store or handle encoder.</param>
+ /// <param name="cryptoKeyStore">The OpenID Provider's association store or handle encoder.</param>
/// <param name="nonceStore">The nonce store to use.</param>
/// <param name="securitySettings">The security settings.</param>
- internal OpenIdChannel(IProviderAssociationStore associationStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings)
- : this(associationStore, nonceStore, new OpenIdMessageFactory(), securitySettings) {
- Contract.Requires<ArgumentNullException>(associationStore != null, "associationStore");
+ internal OpenIdChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings)
+ : this(cryptoKeyStore, nonceStore, new OpenIdMessageFactory(), securitySettings) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
Contract.Requires<ArgumentNullException>(securitySettings != null);
}
@@ -87,13 +87,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// Initializes a new instance of the <see cref="OpenIdChannel"/> class
/// for use by a Provider.
/// </summary>
- /// <param name="associationStore">The association store to use.</param>
+ /// <param name="cryptoKeyStore">The association store to use.</param>
/// <param name="nonceStore">The nonce store to use.</param>
/// <param name="messageTypeProvider">An object that knows how to distinguish the various OpenID message types for deserialization purposes.</param>
/// <param name="securitySettings">The security settings.</param>
- private OpenIdChannel(IProviderAssociationStore associationStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, ProviderSecuritySettings securitySettings) :
- this(messageTypeProvider, InitializeBindingElements(associationStore, nonceStore, securitySettings)) {
- Contract.Requires<ArgumentNullException>(associationStore != null, "associationStore");
+ private OpenIdChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, ProviderSecuritySettings securitySettings) :
+ this(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings)) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
Contract.Requires<ArgumentNullException>(messageTypeProvider != null);
Contract.Requires<ArgumentNullException>(securitySettings != null);
}
@@ -358,19 +358,19 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// <summary>
/// Initializes the binding elements.
/// </summary>
- /// <param name="associationStore">The OpenID Provider's association store or handle encoder.</param>
+ /// <param name="cryptoKeyStore">The OpenID Provider's crypto key store.</param>
/// <param name="nonceStore">The nonce store to use.</param>
/// <param name="securitySettings">The security settings to apply. Must be an instance of either <see cref="RelyingPartySecuritySettings"/> or <see cref="ProviderSecuritySettings"/>.</param>
/// <returns>
/// An array of binding elements which may be used to construct the channel.
/// </returns>
- private static IChannelBindingElement[] InitializeBindingElements(IProviderAssociationStore associationStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings) {
- Contract.Requires<ArgumentNullException>(associationStore != null, "associationStore");
+ private static IChannelBindingElement[] InitializeBindingElements(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
Contract.Requires<ArgumentNullException>(securitySettings != null);
Contract.Requires<ArgumentNullException>(nonceStore != null, "nonceStore");
SigningBindingElement signingElement;
- signingElement = new SigningBindingElement(associationStore, securitySettings);
+ signingElement = new SigningBindingElement(cryptoKeyStore, securitySettings);
var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration();
diff --git a/src/DotNetOpenAuth/OpenId/Provider/IProviderApplicationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/IProviderApplicationStore.cs
index b8d40e6..1bdbe43 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/IProviderApplicationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/IProviderApplicationStore.cs
@@ -14,6 +14,6 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// <summary>
/// A hybrid of all the store interfaces that an OpenID Provider must implement.
/// </summary>
- public interface IProviderApplicationStore : IProviderAssociationStore, INonceStore {
+ public interface IProviderApplicationStore : ICryptoKeyStore, INonceStore {
}
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/IProviderAssociationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/IProviderAssociationStore.cs
index 6cbe52b..b63e2ee 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/IProviderAssociationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/IProviderAssociationStore.cs
@@ -25,7 +25,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// be confidential.
/// </remarks>
[ContractClass(typeof(IProviderAssociationStoreContract))]
- public interface IProviderAssociationStore {
+ internal interface IProviderAssociationStore {
/// <summary>
/// Stores an association and returns a handle for it.
/// </summary>
diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
index dbde982..8be9a0b 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
@@ -64,7 +64,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// </summary>
/// <param name="applicationStore">The application store to use. Cannot be null.</param>
public OpenIdProvider(IProviderApplicationStore applicationStore)
- : this((INonceStore)applicationStore, (IProviderAssociationStore)applicationStore) {
+ : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore) {
Contract.Requires<ArgumentNullException>(applicationStore != null);
Contract.Ensures(this.SecuritySettings != null);
Contract.Ensures(this.Channel != null);
@@ -74,20 +74,21 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// Initializes a new instance of the <see cref="OpenIdProvider"/> class.
/// </summary>
/// <param name="nonceStore">The nonce store to use. Cannot be null.</param>
- private OpenIdProvider(INonceStore nonceStore, IProviderAssociationStore associationStore) {
- Contract.Requires<ArgumentNullException>(nonceStore != null);
- Contract.Requires<ArgumentNullException>(associationStore != null, "associationStore");
+ private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) {
+ Contract.Requires<ArgumentNullException>(nonceStore != null, "nonceStore");
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "associationStore");
Contract.Ensures(this.SecuritySettings != null);
Contract.Ensures(this.Channel != null);
- this.AssociationStore = associationStore;
this.SecuritySettings = DotNetOpenAuthSection.Configuration.OpenId.Provider.SecuritySettings.CreateSecuritySettings();
this.behaviors.CollectionChanged += this.OnBehaviorsChanged;
foreach (var behavior in DotNetOpenAuthSection.Configuration.OpenId.Provider.Behaviors.CreateInstances(false)) {
this.behaviors.Add(behavior);
}
+ this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings);
this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings);
+ this.CryptoKeyStore = cryptoKeyStore;
Reporting.RecordFeatureAndDependencyUse(this, nonceStore);
}
@@ -164,9 +165,21 @@ namespace DotNetOpenAuth.OpenId.Provider {
}
/// <summary>
- /// Gets or sets the association store.
+ /// Gets the crypto key store.
/// </summary>
- public IProviderAssociationStore AssociationStore { get; set; }
+ public ICryptoKeyStore CryptoKeyStore { get; private set; }
+
+ /// <summary>
+ /// Gets the association store.
+ /// </summary>
+ internal IProviderAssociationStore AssociationStore { get; private set; }
+
+ /// <summary>
+ /// Gets the channel.
+ /// </summary>
+ internal OpenIdChannel OpenIdChannel {
+ get { return (OpenIdChannel)this.Channel; }
+ }
/// <summary>
/// Gets the list of services that can perform discovery on identifiers given to this relying party.
@@ -553,5 +566,39 @@ namespace DotNetOpenAuth.OpenId.Provider {
Reporting.RecordFeatureUse(profile);
}
}
+
+ /// <summary>
+ /// Provides a single OP association store instance that can handle switching between
+ /// association handle encoding modes.
+ /// </summary>
+ private class SwitchingAssociationStore : IProviderAssociationStore {
+ private readonly ProviderSecuritySettings securitySettings;
+
+ private IProviderAssociationStore associationHandleEncoder;
+
+ private IProviderAssociationStore associationSecretStorage;
+
+
+ internal SwitchingAssociationStore(ICryptoKeyStore cryptoKeyStore, ProviderSecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
+ Contract.Requires<ArgumentNullException>(securitySettings != null, "securitySettings");
+ this.securitySettings = securitySettings;
+
+ this.associationHandleEncoder = new ProviderAssociationHandleEncoder(cryptoKeyStore);
+ this.associationSecretStorage = new ProviderAssociationKeyStorage(cryptoKeyStore);
+ }
+
+ internal IProviderAssociationStore AssociationStore {
+ get { return this.securitySettings.EncodeAssociationSecretsInHandles ? this.associationHandleEncoder : this.associationSecretStorage; }
+ }
+
+ public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ return this.AssociationStore.Serialize(secret, expiresUtc, privateAssociation);
+ }
+
+ public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
+ return this.AssociationStore.Deserialize(containingMessage, isPrivateAssociation, handle);
+ }
+ }
}
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
index 032a445..35f2303 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
@@ -6,7 +6,9 @@
namespace DotNetOpenAuth.OpenId.Provider {
using System;
+ using System.Diagnostics.Contracts;
using System.Threading;
+ using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging;
/// <summary>
@@ -14,46 +16,16 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// details in the handle.
/// </summary>
public class ProviderAssociationHandleEncoder : IProviderAssociationStore {
- /// <summary>
- /// The thread synchronization object.
- /// </summary>
- private readonly object syncObject = new object();
+ internal const string AssociationHandleEncodingSecretBucket = "https://localhost/dnoa/association_handles";
- /// <summary>
- /// Backing field for the <see cref="Secret"/> property.
- /// </summary>
- private byte[] secret;
+ private readonly ICryptoKeyStore cryptoKeyStore;
/// <summary>
/// Initializes a new instance of the <see cref="ProviderAssociationHandleEncoder"/> class.
/// </summary>
- public ProviderAssociationHandleEncoder() {
- }
-
- /// <summary>
- /// Gets or sets the symmetric secret this Provider uses for protecting messages to itself.
- /// </summary>
- /// <remarks>
- /// If the value is not set by the time this property is requested, a random key will be generated.
- /// </remarks>
- public byte[] Secret {
- get {
- if (this.secret == null) {
- lock (this.syncObject) {
- if (this.secret == null) {
- Logger.OpenId.Info("Generating a symmetric secret for signing and encrypting association handles.");
- this.secret = MessagingUtilities.GetCryptoRandomData(32); // 256-bit symmetric key protects association secrets.
- }
- }
- }
-
- return this.secret;
- }
-
- set {
- ErrorUtilities.VerifyOperation(this.secret == null, "The symmetric secret has already been set.");
- this.secret = value;
- }
+ public ProviderAssociationHandleEncoder(ICryptoKeyStore cryptoKeyStore) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
+ this.cryptoKeyStore = cryptoKeyStore;
}
/// <summary>
@@ -71,8 +43,10 @@ namespace DotNetOpenAuth.OpenId.Provider {
IsPrivateAssociation = isPrivateAssociation,
ExpiresUtc = expiresUtc,
};
- var formatter = AssociationDataBag.CreateFormatter(this.Secret);
- return formatter.Serialize(associationDataBag);
+
+ var encodingSecret = this.cryptoKeyStore.GetCurrentKey(AssociationHandleEncodingSecretBucket, DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime);
+ var formatter = AssociationDataBag.CreateFormatter(encodingSecret.Value.Key);
+ return encodingSecret.Key + "!" + formatter.Serialize(associationDataBag);
}
/// <summary>
@@ -86,10 +60,20 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// </returns>
/// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
- var formatter = AssociationDataBag.CreateFormatter(this.Secret);
+ int privateHandleIndex = handle.IndexOf('!');
+ ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, containingMessage.GetProtocol().openid.assoc_handle, handle);
+ string privateHandle = handle.Substring(0, privateHandleIndex);
+ string encodedHandle = handle.Substring(privateHandleIndex + 1);
+ var encodingSecret = this.cryptoKeyStore.GetKey(AssociationHandleEncodingSecretBucket, privateHandle);
+ if (encodingSecret == null) {
+ Logger.OpenId.Error("Rejecting an association because the symmetric secret it was encoded with is missing or has expired.");
+ return null;
+ }
+
+ var formatter = AssociationDataBag.CreateFormatter(encodingSecret.Key);
AssociationDataBag bag;
try {
- bag = formatter.Deserialize(containingMessage, handle);
+ bag = formatter.Deserialize(containingMessage, encodedHandle);
} catch (ProtocolException) {
return null;
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationKeyStorage.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationKeyStorage.cs
new file mode 100644
index 0000000..4626e88
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationKeyStorage.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProviderAssociationKeyStorage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+
+ internal class ProviderAssociationKeyStorage : IProviderAssociationStore {
+ private const string SharedAssociationBucket = "https://localhost/dnoa/shared_associations";
+
+ private const string PrivateAssociationBucket = "https://localhost/dnoa/private_associations";
+
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ internal ProviderAssociationKeyStorage(ICryptoKeyStore cryptoKeyStore) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
+ this.cryptoKeyStore = cryptoKeyStore;
+ }
+
+ public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) {
+ string handle;
+ this.cryptoKeyStore.StoreKey(
+ privateAssociation ? PrivateAssociationBucket : SharedAssociationBucket,
+ handle = OpenIdUtilities.GenerateRandomAssociationHandle(),
+ new CryptoKey(secret, expiresUtc));
+ return handle;
+ }
+
+ public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
+ var key = this.cryptoKeyStore.GetKey(isPrivateAssociation ? PrivateAssociationBucket : SharedAssociationBucket, handle);
+ if (key != null) {
+ return Association.Deserialize(handle, key.ExpiresUtc, key.Key);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderSecuritySettings.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderSecuritySettings.cs
index d5fa4a9..130e6dd 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/ProviderSecuritySettings.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderSecuritySettings.cs
@@ -24,6 +24,11 @@ namespace DotNetOpenAuth.OpenId.Provider {
internal const bool ProtectDownlevelReplayAttacksDefault = true;
/// <summary>
+ /// The default value for the <see cref="EncodeAssociationSecretsInHandles"/> property.
+ /// </summary>
+ internal const bool EncodeAssociationSecretsInHandlesDefault = true;
+
+ /// <summary>
/// The default value for the <see cref="SignOutgoingExtensions"/> property.
/// </summary>
internal const bool SignOutgoingExtensionsDefault = true;
@@ -102,6 +107,14 @@ namespace DotNetOpenAuth.OpenId.Provider {
public UnsolicitedAssertionVerificationLevel UnsolicitedAssertionVerification { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether the Provider should ease the burden of storing associations
+ /// by encoding them in signed, encrypted form into the association handles themselves, storing only
+ /// a few rotating, private symmetric keys in the Provider's store instead.
+ /// </summary>
+ /// <value>The default value for this property is <c>true</c>.</value>
+ public bool EncodeAssociationSecretsInHandles { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether OpenID 1.x relying parties that may not be
/// protecting their users from replay attacks are protected from
/// replay attacks by this provider.
diff --git a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
index 13b3787..8ead116 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
@@ -21,17 +21,20 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// <see cref="IProviderApplicationStore"/> interface to use instead of this
/// class.
/// </remarks>
- public class StandardProviderApplicationStore : ProviderAssociationHandleEncoder, IProviderApplicationStore {
+ public class StandardProviderApplicationStore : IProviderApplicationStore {
/// <summary>
/// The nonce store to use.
/// </summary>
private readonly INonceStore nonceStore;
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
/// <summary>
/// Initializes a new instance of the <see cref="StandardProviderApplicationStore"/> class.
/// </summary>
public StandardProviderApplicationStore() {
this.nonceStore = new NonceMemoryStore(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime);
+ this.cryptoKeyStore = new MemoryCryptoKeyStore();
}
#region INonceStore Members
@@ -59,5 +62,25 @@ namespace DotNetOpenAuth.OpenId.Provider {
}
#endregion
+
+ #region ICryptoKeyStore
+
+ public CryptoKey GetKey(string bucket, string handle) {
+ return this.cryptoKeyStore.GetKey(bucket, handle);
+ }
+
+ public System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return this.cryptoKeyStore.GetKeys(bucket);
+ }
+
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ this.cryptoKeyStore.StoreKey(bucket, handle, key);
+ }
+
+ public void RemoveKey(string bucket, string handle) {
+ this.cryptoKeyStore.RemoveKey(bucket, handle);
+ }
+
+ #endregion
}
}