diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2011-05-20 08:26:54 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2011-05-20 08:26:54 -0700 |
commit | 9aa1e046c6ccd6fa0859709419c091bf6b52f465 (patch) | |
tree | 739c8704e96cec4ee1eb9846e854d32478b856f5 /src | |
parent | 9ba3d8e9f066132c68501fbc191acd50fba905f4 (diff) | |
download | DotNetOpenAuth-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')
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 } } |