summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs133
-rw-r--r--src/DotNetOpenAuth/CryptoKeyCollisionException.cs70
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj4
-rw-r--r--src/DotNetOpenAuth/ICryptoKeyStore.cs1
-rw-r--r--src/DotNetOpenAuth/MemoryCryptoKeyStore.cs156
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs51
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs20
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs40
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs133
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs12
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs103
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs63
13 files changed, 371 insertions, 417 deletions
diff --git a/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs
index 28f65d9..203d6ab 100644
--- a/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs
+++ b/src/DotNetOpenAuth/AsymmetricCryptoKeyStoreWrapper.cs
@@ -12,6 +12,7 @@ namespace DotNetOpenAuth {
using System.Linq;
using System.Security.Cryptography;
using System.Text;
+ using DotNetOpenAuth.Messaging;
/// <summary>
/// Provides RSA encryption of symmetric keys to protect them from a theft of
@@ -19,32 +20,19 @@ namespace DotNetOpenAuth {
/// </summary>
public class AsymmetricCryptoKeyStoreWrapper : ICryptoKeyStore {
/// <summary>
- /// How frequently to check for and remove expired secrets.
- /// </summary>
- private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30);
-
- /// <summary>
/// The persistent store for asymmetrically encrypted symmetric keys.
/// </summary>
private readonly ICryptoKeyStore dataStore;
/// <summary>
- /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys.
- /// </summary>
- private readonly RSACryptoServiceProvider asymmetricCrypto;
-
- /// <summary>
- /// An in-memory cache of decrypted symmetric keys.
+ /// The memory cache of decrypted keys.
/// </summary>
- /// <remarks>
- /// The key is the bucket name. The value is a dictionary whose key is the handle and whose value is the cached key.
- /// </remarks>
- private readonly Dictionary<string, Dictionary<string, CachedCryptoKey>> decryptedKeyCache = new Dictionary<string, Dictionary<string, CachedCryptoKey>>(StringComparer.Ordinal);
+ private readonly MemoryCryptoKeyStore cache = new MemoryCryptoKeyStore();
/// <summary>
- /// The last time the cache had expired keys removed from it.
+ /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys.
/// </summary>
- private DateTime lastCleaning = DateTime.UtcNow;
+ private readonly RSACryptoServiceProvider asymmetricCrypto;
/// <summary>
/// Initializes a new instance of the <see cref="AsymmetricCryptoKeyStoreWrapper"/> class.
@@ -95,9 +83,7 @@ namespace DotNetOpenAuth {
var encryptedCryptoKey = new CryptoKey(encryptedKey, decryptedCryptoKey.ExpiresUtc);
this.dataStore.StoreKey(bucket, handle, encryptedCryptoKey);
- this.CacheKey(bucket, handle, decryptedCryptoKey, encryptedCryptoKey);
-
- this.CleanExpiredKeysFromMemoryCacheIfAppropriate();
+ this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey));
}
/// <summary>
@@ -107,36 +93,7 @@ namespace DotNetOpenAuth {
/// <param name="handle">The key handle. Case sensitive.</param>
public void RemoveKey(string bucket, string handle) {
this.dataStore.RemoveKey(bucket, handle);
-
- lock (this.decryptedKeyCache) {
- Dictionary<string, CachedCryptoKey> cacheBucket;
- if (this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
- cacheBucket.Remove(handle);
- }
- }
- }
-
- /// <summary>
- /// Caches an encrypted/decrypted key pair.
- /// </summary>
- /// <param name="bucket">The bucket.</param>
- /// <param name="handle">The handle.</param>
- /// <param name="decryptedCryptoKey">The decrypted crypto key.</param>
- /// <param name="encryptedCryptoKey">The encrypted crypto key.</param>
- private void CacheKey(string bucket, string handle, CryptoKey decryptedCryptoKey, CryptoKey encryptedCryptoKey) {
- Contract.Requires(!String.IsNullOrEmpty(bucket));
- Contract.Requires(!String.IsNullOrEmpty(handle));
- Contract.Requires(decryptedKeyCache != null);
- Contract.Requires(encryptedCryptoKey != null);
-
- lock (this.decryptedKeyCache) {
- Dictionary<string, CachedCryptoKey> cacheBucket;
- if (!this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
- this.decryptedKeyCache[bucket] = cacheBucket = new Dictionary<string, CachedCryptoKey>(StringComparer.Ordinal);
- }
-
- cacheBucket[handle] = new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey);
- }
+ this.cache.RemoveKey(bucket, handle);
}
/// <summary>
@@ -150,95 +107,42 @@ namespace DotNetOpenAuth {
}
// Avoid the asymmetric decryption if possible by looking up whether we have that in our cache.
- CachedCryptoKey cached;
- lock (this.decryptedKeyCache) {
- Dictionary<string, CachedCryptoKey> cacheBucket;
- if (this.decryptedKeyCache.TryGetValue(bucket, out cacheBucket)) {
- if (cacheBucket.TryGetValue(handle, out cached) && encryptedCryptoKey.Equals(cached.Encrypted)) {
- return cached.Decrypted;
- }
- }
+ CachedCryptoKey cached = (CachedCryptoKey)this.cache.GetKey(bucket, handle);
+ if (cached != null && MessagingUtilities.AreEquivalent(cached.EncryptedKey, encryptedCryptoKey.Key)) {
+ return cached;
}
byte[] decryptedKey = this.asymmetricCrypto.Decrypt(encryptedCryptoKey.Key, true);
var decryptedCryptoKey = new CryptoKey(decryptedKey, encryptedCryptoKey.ExpiresUtc);
// Store the decrypted version in the cache to save time next time.
- this.CacheKey(bucket, handle, decryptedCryptoKey, encryptedCryptoKey);
+ this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey));
return decryptedCryptoKey;
}
/// <summary>
- /// Cleans the expired keys from memory cache if the cleaning interval has passed.
- /// </summary>
- private void CleanExpiredKeysFromMemoryCacheIfAppropriate() {
- if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
- lock (this.decryptedKeyCache) {
- if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
- this.ClearExpiredKeysFromMemoryCache();
- }
- }
- }
- }
-
- /// <summary>
- /// Weeds out expired keys from the in-memory cache.
- /// </summary>
- private void ClearExpiredKeysFromMemoryCache() {
- lock (this.decryptedKeyCache) {
- var emptyBuckets = new List<string>();
- foreach (var bucketPair in this.decryptedKeyCache) {
- var expiredKeys = new List<string>();
- foreach (var handlePair in bucketPair.Value) {
- if (handlePair.Value.Encrypted.ExpiresUtc < DateTime.UtcNow) {
- expiredKeys.Add(handlePair.Key);
- }
- }
-
- foreach (var expiredKey in expiredKeys) {
- bucketPair.Value.Remove(expiredKey);
- }
-
- if (bucketPair.Value.Count == 0) {
- emptyBuckets.Add(bucketPair.Key);
- }
- }
-
- foreach (string emptyBucket in emptyBuckets) {
- this.decryptedKeyCache.Remove(emptyBucket);
- }
-
- this.lastCleaning = DateTime.UtcNow;
- }
- }
-
- /// <summary>
/// An encrypted key and its decrypted equivalent.
/// </summary>
- private class CachedCryptoKey {
+ private class CachedCryptoKey : CryptoKey {
/// <summary>
/// Initializes a new instance of the <see cref="CachedCryptoKey"/> class.
/// </summary>
/// <param name="encrypted">The encrypted key.</param>
/// <param name="decrypted">The decrypted key.</param>
- internal CachedCryptoKey(CryptoKey encrypted, CryptoKey decrypted) {
+ internal CachedCryptoKey(CryptoKey encrypted, CryptoKey decrypted)
+ : base(decrypted.Key, decrypted.ExpiresUtc) {
Contract.Requires(encrypted != null);
Contract.Requires(decrypted != null);
+ Contract.Requires(encrypted.ExpiresUtc == decrypted.ExpiresUtc);
- this.Encrypted = encrypted;
- this.Decrypted = decrypted;
+ this.EncryptedKey = encrypted.Key;
}
/// <summary>
/// Gets or sets the encrypted key.
/// </summary>
- internal CryptoKey Encrypted { get; private set; }
-
- /// <summary>
- /// Gets or sets the decrypted key.
- /// </summary>
- internal CryptoKey Decrypted { get; private set; }
+ internal byte[] EncryptedKey { get; private set; }
/// <summary>
/// Invariant conditions.
@@ -246,8 +150,7 @@ namespace DotNetOpenAuth {
[ContractInvariantMethod]
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
private void ObjectInvariant() {
- Contract.Invariant(this.Encrypted != null);
- Contract.Invariant(this.Decrypted != null);
+ Contract.Invariant(this.EncryptedKey != null);
}
}
}
diff --git a/src/DotNetOpenAuth/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth/CryptoKeyCollisionException.cs
new file mode 100644
index 0000000..e68e022
--- /dev/null
+++ b/src/DotNetOpenAuth/CryptoKeyCollisionException.cs
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------
+// <copyright file="CryptoKeyCollisionException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Security.Permissions;
+
+ [Serializable]
+ public class CryptoKeyCollisionException : ArgumentException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ public CryptoKeyCollisionException() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ /// <param name="message">A message describing the specific error the occurred or was detected.</param>
+ public CryptoKeyCollisionException(string message) : base(message) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ /// <param name="message">A message describing the specific error the occurred or was detected.</param>
+ /// <param name="inner">The inner exception to include.</param>
+ public CryptoKeyCollisionException(string message, Exception inner) : base(message, inner) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected CryptoKeyCollisionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with information about the exception.
+ /// </summary>
+ /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic).
+ /// </exception>
+ /// <PermissionSet>
+ /// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/>
+ /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/>
+ /// </PermissionSet>
+#if CLR4
+ [SecurityCritical]
+#else
+ [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
+#endif
+ public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) {
+ base.GetObjectData(info, context);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 2dc5653..c91f92b 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -307,6 +307,7 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="Configuration\HostNameElement.cs" />
<Compile Include="Configuration\XriResolverElement.cs" />
<Compile Include="CryptoKey.cs" />
+ <Compile Include="CryptoKeyCollisionException.cs" />
<Compile Include="ICryptoKeyStore.cs" />
<Compile Include="IEmbeddedResourceRetrieval.cs" />
<Compile Include="InfoCard\ClaimType.cs" />
@@ -321,6 +322,7 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="InfoCard\Token\TokenUtility.cs" />
<Compile Include="InfoCard\Token\TokenDecryptor.cs" />
<Compile Include="InfoCard\WellKnownIssuers.cs" />
+ <Compile Include="MemoryCryptoKeyStore.cs" />
<Compile Include="Messaging\BinaryDataBagFormatter.cs" />
<Compile Include="Messaging\CachedDirectWebResponse.cs" />
<Compile Include="Messaging\ChannelContract.cs" />
@@ -500,7 +502,6 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="OpenId\Association.cs" />
<Compile Include="OpenId\Provider\AssociationDataBag.cs" />
<Compile Include="OpenId\Provider\IProviderAssociationStore.cs" />
- <Compile Include="OpenId\RelyingParty\AssociationMemoryStore.cs" />
<Compile Include="OpenId\Provider\ProviderAssociationHandleEncoder.cs" />
<Compile Include="OpenId\RelyingParty\CryptoKeyStoreAsRelyingPartyAssociationStore.cs" />
<Compile Include="OpenId\RelyingParty\IRelyingPartyAssociationStore.cs" />
@@ -660,7 +661,6 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" />
<Compile Include="OpenId\RelyingParty\IRelyingPartyApplicationStore.cs" />
<Compile Include="OpenId\RelyingParty\PositiveAuthenticationResponseSnapshot.cs" />
- <Compile Include="OpenId\RelyingParty\PrivateSecretManager.cs" />
<Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" />
<Compile Include="OpenId\IdentifierDiscoveryResult.cs" />
<Compile Include="OpenId\OpenIdXrdsHelper.cs" />
diff --git a/src/DotNetOpenAuth/ICryptoKeyStore.cs b/src/DotNetOpenAuth/ICryptoKeyStore.cs
index 4f221be..d2a5147 100644
--- a/src/DotNetOpenAuth/ICryptoKeyStore.cs
+++ b/src/DotNetOpenAuth/ICryptoKeyStore.cs
@@ -45,6 +45,7 @@ namespace DotNetOpenAuth {
/// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
/// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
/// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
void StoreKey(string bucket, string handle, CryptoKey key);
/// <summary>
diff --git a/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs b/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs
new file mode 100644
index 0000000..fcf1f23
--- /dev/null
+++ b/src/DotNetOpenAuth/MemoryCryptoKeyStore.cs
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------
+// <copyright file="MemoryCryptoKeyStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ /// <summary>
+ /// A in-memory store of crypto keys.
+ /// </summary>
+ internal class MemoryCryptoKeyStore : ICryptoKeyStore {
+ /// <summary>
+ /// How frequently to check for and remove expired secrets.
+ /// </summary>
+ private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30);
+
+ /// <summary>
+ /// An in-memory cache of decrypted symmetric keys.
+ /// </summary>
+ /// <remarks>
+ /// The key is the bucket name. The value is a dictionary whose key is the handle and whose value is the cached key.
+ /// </remarks>
+ private readonly Dictionary<string, Dictionary<string, CryptoKey>> store = new Dictionary<string, Dictionary<string, CryptoKey>>(StringComparer.Ordinal);
+
+ /// <summary>
+ /// The last time the cache had expired keys removed from it.
+ /// </summary>
+ private DateTime lastCleaning = DateTime.UtcNow;
+
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ CryptoKey key;
+ if (cacheBucket.TryGetValue(handle, out key)) {
+ return key;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ return cacheBucket.ToList();
+ } else {
+ return Enumerable.Empty<KeyValuePair<string, CryptoKey>>();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (!this.store.TryGetValue(bucket, out cacheBucket)) {
+ this.store[bucket] = cacheBucket = new Dictionary<string, CryptoKey>(StringComparer.Ordinal);
+ }
+
+ if (cacheBucket.ContainsKey(handle)) {
+ throw new CryptoKeyCollisionException();
+ }
+
+ cacheBucket[handle] = key;
+
+ this.CleanExpiredKeysFromMemoryCacheIfAppropriate();
+ }
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ cacheBucket.Remove(handle);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Cleans the expired keys from memory cache if the cleaning interval has passed.
+ /// </summary>
+ private void CleanExpiredKeysFromMemoryCacheIfAppropriate() {
+ if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
+ lock (this.store) {
+ if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
+ this.ClearExpiredKeysFromMemoryCache();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Weeds out expired keys from the in-memory cache.
+ /// </summary>
+ private void ClearExpiredKeysFromMemoryCache() {
+ lock (this.store) {
+ var emptyBuckets = new List<string>();
+ foreach (var bucketPair in this.store) {
+ var expiredKeys = new List<string>();
+ foreach (var handlePair in bucketPair.Value) {
+ if (handlePair.Value.ExpiresUtc < DateTime.UtcNow) {
+ expiredKeys.Add(handlePair.Key);
+ }
+ }
+
+ foreach (var expiredKey in expiredKeys) {
+ bucketPair.Value.Remove(expiredKey);
+ }
+
+ if (bucketPair.Value.Count == 0) {
+ emptyBuckets.Add(bucketPair.Key);
+ }
+ }
+
+ foreach (string emptyBucket in emptyBuckets) {
+ this.store.Remove(emptyBucket);
+ }
+
+ this.lastCleaning = DateTime.UtcNow;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index 2119d78..3062f23 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -27,6 +27,13 @@ namespace DotNetOpenAuth.Messaging {
/// A grab-bag of utility methods useful for the channel stack of the protocol.
/// </summary>
public static class MessagingUtilities {
+ private const int SymmetricSecretHandleLength = 4; // TODO: replace this with an unnamed secret concept.
+
+ /// <summary>
+ /// The default lifetime of a private secret.
+ /// </summary>
+ private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.PrivateSecretMaximumAge;
+
/// <summary>
/// The cryptographically strong random data generator used for creating secrets.
/// </summary>
@@ -54,6 +61,11 @@ namespace DotNetOpenAuth.Messaging {
internal const string Digits = "0123456789";
/// <summary>
+ /// The set of digits and alphabetic letters (upper and lowercase).
+ /// </summary>
+ internal const string AlphaNumeric = UppercaseLetters + LowercaseLetters + Digits;
+
+ /// <summary>
/// The set of digits, and alphabetic letters (upper and lowercase) that are clearly
/// visually distinguishable.
/// </summary>
@@ -654,6 +666,45 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Gets a key from a given bucket with the longest remaining life, or creates a new one if necessary.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ /// <param name="bucket">The bucket where the key should be found or stored.</param>
+ /// <param name="minimumRemainingLife">The minimum remaining life required on the returned key.</param>
+ /// <param name="keySize">The required size of the key, in bits.</param>
+ /// <returns>
+ /// A key-value pair whose key is the secret's handle and whose value is the cryptographic key.
+ /// </returns>
+ internal static KeyValuePair<string, CryptoKey> GetCurrentKey(this ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan minimumRemainingLife, int keySize = 256) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Requires<ArgumentException>(keySize % 8 == 0);
+
+ var cryptoKeyPair = cryptoKeyStore.GetKeys(bucket).FirstOrDefault(pair => pair.Value.Key.Length == keySize / 8);
+ if (cryptoKeyPair.Value == null || cryptoKeyPair.Value.ExpiresUtc < DateTime.UtcNow + minimumRemainingLife) {
+ // No key exists with enough remaining life for the required purpose. Create a new key.
+ ErrorUtilities.VerifyProtocol(minimumRemainingLife <= SymmetricSecretKeyLifespan, "Unable to create a new symmetric key with the required lifespan of {0} because it is beyond the limit of {1}.", minimumRemainingLife, SymmetricSecretKeyLifespan);
+ byte[] secret = GetCryptoRandomData(keySize / 8);
+ DateTime expires = DateTime.UtcNow + SymmetricSecretKeyLifespan;
+ var cryptoKey = new CryptoKey(secret, expires);
+
+ // Store this key so we can find and use it later.
+ int failedAttempts = 0;
+ tryAgain:
+ try {
+ string handle = GetRandomString(SymmetricSecretHandleLength, AlphaNumeric);
+ cryptoKeyPair = new KeyValuePair<string, CryptoKey>(handle, cryptoKey);
+ cryptoKeyStore.StoreKey(bucket, handle, cryptoKey);
+ } catch (CryptoKeyCollisionException) {
+ ErrorUtilities.VerifyInternal(++failedAttempts < 3, "Unable to derive a unique handle to a private symmetric key.");
+ goto tryAgain;
+ }
+ }
+
+ return cryptoKeyPair;
+ }
+
+ /// <summary>
/// Compresses a given buffer.
/// </summary>
/// <param name="buffer">The buffer to compress.</param>
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
index 0acd796..072ebe0 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
@@ -46,11 +46,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// Initializes a new instance of the <see cref="OpenIdChannel"/> class
/// for use by a Relying Party.
/// </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="securitySettings">The security settings to apply.</param>
- internal OpenIdChannel(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings)
- : this(associationStore, nonceStore, new OpenIdMessageFactory(), securitySettings, false) {
+ internal OpenIdChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings)
+ : this(cryptoKeyStore, nonceStore, new OpenIdMessageFactory(), securitySettings, false) {
Contract.Requires<ArgumentNullException>(securitySettings != null);
}
@@ -71,13 +71,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// Initializes a new instance of the <see cref="OpenIdChannel"/> class
/// for use by a Relying Party.
/// </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 to apply.</param>
/// <param name="nonVerifying">A value indicating whether the channel is set up with no functional security binding elements.</param>
- private OpenIdChannel(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, RelyingPartySecuritySettings securitySettings, bool nonVerifying) :
- this(messageTypeProvider, InitializeBindingElements(associationStore, nonceStore, securitySettings, nonVerifying)) {
+ private OpenIdChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, RelyingPartySecuritySettings securitySettings, bool nonVerifying) :
+ this(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings, nonVerifying)) {
Contract.Requires<ArgumentNullException>(messageTypeProvider != null);
Contract.Requires<ArgumentNullException>(securitySettings != null);
Contract.Requires<ArgumentException>(!nonVerifying || securitySettings is RelyingPartySecuritySettings);
@@ -311,11 +311,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// <returns>
/// An array of binding elements which may be used to construct the channel.
/// </returns>
- private static IChannelBindingElement[] InitializeBindingElements(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings, bool nonVerifying) {
+ private static IChannelBindingElement[] InitializeBindingElements(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings, bool nonVerifying) {
Contract.Requires<ArgumentNullException>(securitySettings != null);
SigningBindingElement signingElement;
- signingElement = nonVerifying ? null : new SigningBindingElement(associationStore);
+ signingElement = nonVerifying ? null : new SigningBindingElement(new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore));
var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration();
@@ -325,7 +325,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
elements.Add(new BackwardCompatibilityBindingElement());
ReturnToNonceBindingElement requestNonceElement = null;
- if (associationStore != null) {
+ if (cryptoKeyStore != null) {
if (nonceStore != null) {
// There is no point in having a ReturnToNonceBindingElement without
// a ReturnToSignatureBindingElement because the nonce could be
@@ -336,7 +336,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
// It is important that the return_to signing element comes last
// so that the nonce is included in the signature.
- elements.Add(new ReturnToSignatureBindingElement(associationStore, securitySettings));
+ elements.Add(new ReturnToSignatureBindingElement(cryptoKeyStore));
}
ErrorUtilities.VerifyOperation(!securitySettings.RejectUnsolicitedAssertions || requestNonceElement != null, OpenIdStrings.UnsolicitedAssertionRejectionRequiresNonceStore);
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
index eee7509..fea68d0 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
@@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
using System.Diagnostics.Contracts;
using System.Security.Cryptography;
using System.Web;
+ using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Messages;
using DotNetOpenAuth.OpenId.RelyingParty;
@@ -43,19 +44,24 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle";
/// <summary>
- /// The hashing algorithm used to generate the private signature on the return_to parameter.
+ /// The URI to use for private associations at this RP.
/// </summary>
- private PrivateSecretManager secretManager;
+ private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret");
+
+ /// <summary>
+ /// The key store used to generate the private signature on the return_to parameter.
+ /// </summary>
+ private ICryptoKeyStore cryptoKeyStore;
/// <summary>
/// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class.
/// </summary>
/// <param name="secretStore">The secret store from which to retrieve the secret used for signing.</param>
/// <param name="securitySettings">The security settings.</param>
- internal ReturnToSignatureBindingElement(IRelyingPartyAssociationStore secretStore, RelyingPartySecuritySettings securitySettings) {
- Contract.Requires<ArgumentNullException>(secretStore != null);
+ internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null);
- this.secretManager = new PrivateSecretManager(securitySettings, secretStore);
+ this.cryptoKeyStore = cryptoKeyStore;
}
#region IChannelBindingElement Members
@@ -96,8 +102,10 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
SignedResponseRequest request = message as SignedResponseRequest;
if (request != null && request.ReturnTo != null && request.SignReturnTo) {
- request.AddReturnToArguments(ReturnToSignatureHandleParameterName, this.secretManager.CurrentHandle);
- request.AddReturnToArguments(ReturnToSignatureParameterName, this.GetReturnToSignature(request.ReturnTo));
+ var cryptoKeyPair = this.cryptoKeyStore.GetCurrentKey(SecretUri.AbsoluteUri, DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime);
+ request.AddReturnToArguments(ReturnToSignatureHandleParameterName, cryptoKeyPair.Key);
+ string signature = Convert.ToBase64String(this.GetReturnToSignature(request.ReturnTo, cryptoKeyPair.Value));
+ request.AddReturnToArguments(ReturnToSignatureParameterName, signature);
// We return none because we are not signing the entire message (only a part).
return MessageProtections.None;
@@ -134,10 +142,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
// Only check the return_to signature if one is present.
if (returnToParameters[ReturnToSignatureHandleParameterName] != null) {
// Set the safety flag showing whether the return_to url had a valid signature.
- string expected = this.GetReturnToSignature(response.ReturnTo);
+ byte[] expectedBytes = this.GetReturnToSignature(response.ReturnTo);
string actual = returnToParameters[ReturnToSignatureParameterName];
actual = OpenIdUtilities.FixDoublyUriDecodedBase64String(actual);
- response.ReturnToParametersSignatureValidated = actual == expected;
+ byte[] actualBytes = Convert.FromBase64String(actual);
+ response.ReturnToParametersSignatureValidated = MessagingUtilities.AreEquivalentConstantTime(actualBytes, expectedBytes);
if (!response.ReturnToParametersSignatureValidated) {
Logger.Bindings.WarnFormat("The return_to signature failed verification.");
}
@@ -161,7 +170,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// itself, in order that OPs that might change the return_to's implicit port :80 part
/// or other minor changes do not invalidate the signature.
/// </remarks>
- private string GetReturnToSignature(Uri returnTo) {
+ private byte[] GetReturnToSignature(Uri returnTo, CryptoKey cryptoKey = null) {
Contract.Requires<ArgumentNullException>(returnTo != null);
// Assemble the dictionary to sign, taking care to remove the signature itself
@@ -182,12 +191,17 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
byte[] bytesToSign = KeyValueFormEncoding.GetBytes(sortedReturnToParameters);
byte[] signature;
try {
- signature = this.secretManager.Sign(bytesToSign, returnToParameters[ReturnToSignatureHandleParameterName]);
+ if (cryptoKey == null) {
+ cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]);
+ }
+
+ var signer = new HMACSHA256(cryptoKey.Key);
+ signature = signer.ComputeHash(bytesToSign);
} catch (ProtocolException ex) {
throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired);
}
- string signatureBase64 = Convert.ToBase64String(signature);
- return signatureBase64;
+
+ return signature;
}
}
}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs
deleted file mode 100644
index 9224a1a..0000000
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationMemoryStore.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="AssociationMemoryStore.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.OpenId.RelyingParty {
- using System;
- using System.Collections.Generic;
- using System.Linq;
-
- /// <summary>
- /// Manages a set of associations in memory only (no database).
- /// </summary>
- /// <remarks>
- /// This class should be used for low-to-medium traffic relying party sites that can afford to lose associations
- /// if the app pool was ever restarted. High traffic relying parties and providers should write their own
- /// implementation of <see cref="IRelyingPartyAssociationStore"/> that works against their own database schema
- /// to allow for persistance and recall of associations across servers in a web farm and server restarts.
- /// </remarks>
- internal class AssociationMemoryStore : IRelyingPartyAssociationStore {
- /// <summary>
- /// How many association store requests should occur between each spring cleaning.
- /// </summary>
- private const int PeriodicCleaningFrequency = 10;
-
- /// <summary>
- /// For Relying Parties, this maps OP Endpoints to a set of associations with that endpoint.
- /// For Providers, this keeps smart and dumb associations in two distinct pools.
- /// </summary>
- private Dictionary<Uri, Associations> serverAssocsTable = new Dictionary<Uri, Associations>();
-
- /// <summary>
- /// A counter to track how close we are to an expired association cleaning run.
- /// </summary>
- private int periodicCleaning;
-
- /// <summary>
- /// Stores a given association for later recall.
- /// </summary>
- /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
- /// <param name="association">The association to store.</param>
- public void StoreAssociation(Uri providerEndpoint, Association association) {
- lock (this) {
- if (!this.serverAssocsTable.ContainsKey(providerEndpoint)) {
- this.serverAssocsTable.Add(providerEndpoint, new Associations());
- }
- Associations server_assocs = this.serverAssocsTable[providerEndpoint];
-
- server_assocs.Set(association);
-
- unchecked {
- this.periodicCleaning++;
- }
- if (this.periodicCleaning % PeriodicCleaningFrequency == 0) {
- this.ClearExpiredAssociations();
- }
- }
- }
-
- /// <summary>
- /// Gets the best association (the one with the longest remaining life) for a given key.
- /// </summary>
- /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
- /// <param name="securitySettings">The security settings.</param>
- /// <returns>
- /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
- /// </returns>
- public Association GetAssociation(Uri providerEndpoint, SecuritySettings securitySettings) {
- lock (this) {
- return this.GetServerAssociations(providerEndpoint).Best.FirstOrDefault(assoc => securitySettings.IsAssociationInPermittedRange(assoc));
- }
- }
-
- /// <summary>
- /// Gets the association for a given key and handle.
- /// </summary>
- /// <param name="providerEndpoint">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>
- public Association GetAssociation(Uri providerEndpoint, string handle) {
- lock (this) {
- return this.GetServerAssociations(providerEndpoint).Get(handle);
- }
- }
-
- /// <summary>
- /// Removes a specified handle that may exist in the store.
- /// </summary>
- /// <param name="providerEndpoint">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>
- public bool RemoveAssociation(Uri providerEndpoint, string handle) {
- lock (this) {
- return this.GetServerAssociations(providerEndpoint).Remove(handle);
- }
- }
-
- /// <summary>
- /// Gets the server associations for a given OP Endpoint or dumb/smart mode.
- /// </summary>
- /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
- /// <returns>The collection of associations that fit the <paramref name="providerEndpoint"/>.</returns>
- internal Associations GetServerAssociations(Uri providerEndpoint) {
- lock (this) {
- if (!this.serverAssocsTable.ContainsKey(providerEndpoint)) {
- this.serverAssocsTable.Add(providerEndpoint, new Associations());
- }
-
- return this.serverAssocsTable[providerEndpoint];
- }
- }
-
- /// <summary>
- /// Clears all expired associations from the store.
- /// </summary>
- private void ClearExpiredAssociations() {
- lock (this) {
- foreach (Associations assocs in this.serverAssocsTable.Values) {
- assocs.ClearExpired();
- }
- }
- }
- }
-}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs
index 0c80c0f..2943720 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IRelyingPartyApplicationStore.cs
@@ -16,6 +16,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// A hybrid of all the store interfaces that a Relying Party requires in order
/// to operate in "smart" mode.
/// </summary>
- public interface IRelyingPartyApplicationStore : IRelyingPartyAssociationStore, INonceStore {
+ public interface IRelyingPartyApplicationStore : ICryptoKeyStore, INonceStore {
}
}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
index cf82cf3..6fa2194 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -107,14 +107,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// <summary>
/// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
/// </summary>
- /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
+ /// <param name="cryptoKeyStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
/// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
- private OpenIdRelyingParty(IRelyingPartyAssociationStore associationStore, INonceStore nonceStore) {
+ private OpenIdRelyingParty(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore) {
// If we are a smart-mode RP (supporting associations), then we MUST also be
// capable of storing nonces to prevent replay attacks.
// If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
- Contract.Requires<ArgumentException>(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
+ Contract.Requires<ArgumentException>(cryptoKeyStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings();
@@ -137,10 +137,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
}
- this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings);
- this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings);
+ this.channel = new OpenIdChannel(cryptoKeyStore, nonceStore, this.SecuritySettings);
+ this.AssociationManager = new AssociationManager(this.Channel, new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore), this.SecuritySettings);
- Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore);
+ Reporting.RecordFeatureAndDependencyUse(this, cryptoKeyStore, nonceStore);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs
deleted file mode 100644
index 02cf4c5..0000000
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/PrivateSecretManager.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="PrivateSecretManager.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.OpenId.RelyingParty {
- using System;
- using System.Diagnostics.Contracts;
- using DotNetOpenAuth.Configuration;
- using DotNetOpenAuth.Messaging;
-
- /// <summary>
- /// Manages signing at the RP using private secrets.
- /// </summary>
- internal class PrivateSecretManager {
- /// <summary>
- /// The URI to use for private associations at this RP.
- /// </summary>
- private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret");
-
- /// <summary>
- /// The security settings that apply to this Relying Party.
- /// </summary>
- private RelyingPartySecuritySettings securitySettings;
-
- /// <summary>
- /// The association store
- /// </summary>
- private IRelyingPartyAssociationStore store;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PrivateSecretManager"/> class.
- /// </summary>
- /// <param name="securitySettings">The security settings.</param>
- /// <param name="store">The association store.</param>
- internal PrivateSecretManager(RelyingPartySecuritySettings securitySettings, IRelyingPartyAssociationStore store) {
- Contract.Requires<ArgumentNullException>(securitySettings != null);
- Contract.Requires<ArgumentNullException>(store != null);
-
- this.securitySettings = securitySettings;
- this.store = store;
- }
-
- /// <summary>
- /// Gets the handle of the association to use for private signatures.
- /// </summary>
- /// <returns>
- /// An string made up of plain ASCII characters.
- /// </returns>
- internal string CurrentHandle {
- get {
- Association association = this.GetAssociation();
- return association.Handle;
- }
- }
-
- /// <summary>
- /// Used to verify a signature previously written.
- /// </summary>
- /// <param name="buffer">The data whose signature is to be verified.</param>
- /// <param name="handle">The handle to the private association used to sign the data.</param>
- /// <returns>
- /// The signature for the given buffer using the provided handle.
- /// </returns>
- /// <exception cref="ProtocolException">Thrown when an association with the given handle could not be found.
- /// This most likely happens if the association was near the end of its life and the user took too long to log in.</exception>
- internal byte[] Sign(byte[] buffer, string handle) {
- Contract.Requires<ArgumentNullException>(buffer != null);
- Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle));
-
- Association association = this.store.GetAssociation(SecretUri, handle);
- ErrorUtilities.VerifyProtocol(association != null, OpenIdStrings.PrivateRPSecretNotFound, handle);
- return association.Sign(buffer);
- }
-
- /// <summary>
- /// Gets an association to use for signing new data.
- /// </summary>
- /// <returns>
- /// The association, which may have previously existed or
- /// may have been created as a result of this call.
- /// </returns>
- private Association GetAssociation() {
- Association privateAssociation = this.store.GetAssociation(SecretUri, this.securitySettings);
- if (privateAssociation == null || !privateAssociation.HasUsefulLifeRemaining) {
- int secretLength = HmacShaAssociation.GetSecretLength(Protocol.Default, Protocol.Default.Args.SignatureAlgorithm.Best);
- byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength);
- privateAssociation = HmacShaAssociation.Create(OpenIdUtilities.GenerateRandomAssociationHandle(), secret, this.securitySettings.PrivateSecretMaximumAge);
- if (!privateAssociation.HasUsefulLifeRemaining) {
- Logger.OpenId.WarnFormat(
- "Brand new private association has a shorter lifespan ({0}) than the maximum allowed authentication time for a user ({1}). This may lead to login failures.",
- this.securitySettings.PrivateSecretMaximumAge,
- DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime);
- }
-
- this.store.StoreAssociation(SecretUri, privateAssociation);
- }
-
- return privateAssociation;
- }
- }
-}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs
index 8691662..c3a623b 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs
@@ -6,6 +6,7 @@
namespace DotNetOpenAuth.OpenId.RelyingParty {
using System;
+ using System.Collections.Generic;
using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.OpenId.ChannelElements;
@@ -23,65 +24,59 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// <summary>
/// The association store to use.
/// </summary>
- private readonly IRelyingPartyAssociationStore associationStore;
+ private readonly ICryptoKeyStore keyStore;
/// <summary>
/// Initializes a new instance of the <see cref="StandardRelyingPartyApplicationStore"/> class.
/// </summary>
public StandardRelyingPartyApplicationStore() {
this.nonceStore = new NonceMemoryStore(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime);
- this.associationStore = new AssociationMemoryStore();
+ this.keyStore = new MemoryCryptoKeyStore();
}
- #region IAssociationStore Members
+ #region ICryptoKeyStore Members
/// <summary>
- /// Saves an <see cref="Association"/> for later recall.
+ /// Gets the key in a given bucket and handle.
/// </summary>
- /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for providers).</param>
- /// <param name="association">The association to store.</param>
- public void StoreAssociation(Uri providerEndpoint, Association association) {
- this.associationStore.StoreAssociation(providerEndpoint, association);
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ return this.keyStore.GetKey(bucket, handle);
}
/// <summary>
- /// Gets the best association (the one with the longest remaining life) for a given key.
+ /// Gets a sequence of existing keys within a given bucket.
/// </summary>
- /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
- /// <param name="securityRequirements">The security settings.</param>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
/// <returns>
- /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
/// </returns>
- public Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) {
- return this.associationStore.GetAssociation(providerEndpoint, securityRequirements);
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return this.keyStore.GetKeys(bucket);
}
/// <summary>
- /// Gets the association for a given key and handle.
+ /// Stores a cryptographic key.
/// </summary>
- /// <param name="providerEndpoint">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>
- public Association GetAssociation(Uri providerEndpoint, string handle) {
- return this.associationStore.GetAssociation(providerEndpoint, handle);
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ this.keyStore.StoreKey(bucket, handle, key);
}
/// <summary>
- /// Removes a specified handle that may exist in the store.
+ /// Removes the key.
/// </summary>
- /// <param name="providerEndpoint">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>
- public bool RemoveAssociation(Uri providerEndpoint, string handle) {
- return this.associationStore.RemoveAssociation(providerEndpoint, handle);
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ this.keyStore.RemoveKey(bucket, handle);
}
#endregion