diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2011-05-27 08:02:10 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2011-05-27 08:02:10 -0700 |
commit | be0feb422080002f7984e9a4e0161425df00d137 (patch) | |
tree | 44b0bb946f823182697778fa1bf02e0ce4926bcf /src | |
parent | f922bee2328747e7890778473a2d1c943eaabeb1 (diff) | |
download | DotNetOpenAuth-be0feb422080002f7984e9a4e0161425df00d137.zip DotNetOpenAuth-be0feb422080002f7984e9a4e0161425df00d137.tar.gz DotNetOpenAuth-be0feb422080002f7984e9a4e0161425df00d137.tar.bz2 |
Allowed for cycling of symmetric cryptographic keys by replacing the effectively constant byte[] secret with a new ICryptoKeyStore throughout the OAuth 2 and OpenID stacks.
And StyleCop fixes.
Diffstat (limited to 'src')
18 files changed, 251 insertions, 110 deletions
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index 8b6d6c1..065b5ee 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -236,7 +236,7 @@ </xs:documentation> </xs:annotation> </xs:attribute> - <xs:attribute name="privateSecretMaximumAge" type="xs:string"> + <xs:attribute name="privateSecretMaximumAge" type="xs:string" default="28.00:00:00"> <xs:annotation> <xs:documentation> The maximum age of a secret used for private signing or encryption before it is renewed. diff --git a/src/DotNetOpenAuth/Configuration/MessagingElement.cs b/src/DotNetOpenAuth/Configuration/MessagingElement.cs index d85f799..1c46bcf 100644 --- a/src/DotNetOpenAuth/Configuration/MessagingElement.cs +++ b/src/DotNetOpenAuth/Configuration/MessagingElement.cs @@ -73,8 +73,8 @@ namespace DotNetOpenAuth.Configuration { /// Gets or sets the maximum lifetime of a private symmetric secret, /// that may be used for signing or encryption. /// </summary> - /// <value>The default value is 7 days.</value> - [ConfigurationProperty(PrivateSecretMaximumAgeConfigName, DefaultValue = "07:00:00")] + /// <value>The default value is 28 days (twice the age of the longest association).</value> + [ConfigurationProperty(PrivateSecretMaximumAgeConfigName, DefaultValue = "28.00:00:00")] public TimeSpan PrivateSecretMaximumAge { get { return (TimeSpan)this[PrivateSecretMaximumAgeConfigName]; } set { this[PrivateSecretMaximumAgeConfigName] = value; } diff --git a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs index 8a922f7..0d8e8b4 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs @@ -35,6 +35,9 @@ namespace DotNetOpenAuth.Configuration { /// </summary> private const string AssociationsConfigName = "associations"; + /// <summary> + /// The name of the @encodeAssociationSecretsInHandles attribute. + /// </summary> private const string EncodeAssociationSecretsInHandlesConfigName = "encodeAssociationSecretsInHandles"; /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs b/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs index 08c1219..d44d9bb 100644 --- a/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs +++ b/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs @@ -34,15 +34,17 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Initializes a new instance of the <see cref="UriStyleMessageFormatter<T>"/> class. /// </summary> - /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param> + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param> /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param> /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param> + /// <param name="minimumAge">The minimum age.</param> /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param> /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param> - protected internal BinaryDataBagFormatter(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) - : base(symmetricSecret, signed, encrypted, compressed, maximumAge, decodeOnceOnly) { - Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); + protected internal BinaryDataBagFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) + : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) { + Contract.Requires<ArgumentException>((cryptoKeyStore != null && bucket != null) || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs index 167f7b0..e7dbf46 100644 --- a/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs +++ b/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs @@ -8,6 +8,10 @@ namespace DotNetOpenAuth.Messaging.Bindings { using System; using System.Security.Permissions; + /// <summary> + /// Thrown by a hosting application or web site when a cryptographic key is created with a + /// bucket and handle that conflicts with a previously stored and unexpired key. + /// </summary> [Serializable] public class CryptoKeyCollisionException : ArgumentException { /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs index 98f5e8c..b10a36f 100644 --- a/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs +++ b/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs @@ -33,14 +33,19 @@ namespace DotNetOpenAuth.Messaging { private const int NonceLength = 6; /// <summary> - /// The symmetric secret used for signing/encryption of verification codes and refresh tokens. + /// The minimum allowable lifetime for the key used to encrypt/decrypt or sign this databag. /// </summary> - private readonly byte[] symmetricSecret; + private readonly TimeSpan minimumAge = TimeSpan.FromDays(1); /// <summary> - /// The hashing algorithm to use while signing when using a symmetric secret. + /// The symmetric key store with the secret used for signing/encryption of verification codes and refresh tokens. /// </summary> - private readonly HashAlgorithm symmetricHasher; + private readonly ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// The bucket for symmetric keys. + /// </summary> + private readonly string cryptoKeyBucket; /// <summary> /// The crypto to use for signing access tokens. @@ -100,21 +105,24 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Initializes a new instance of the <see cref="UriStyleMessageFormatter<T>"/> class. /// </summary> - /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param> + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param> /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param> /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param> + /// <param name="minimumAge">The required minimum lifespan within which this token must be decodable and verifiable; useful only when <paramref name="signed"/> and/or <paramref name="encrypted"/> is true.</param> /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param> /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param> - protected DataBagFormatterBase(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) + protected DataBagFormatterBase(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) : this(signed, encrypted, compressed, maximumAge, decodeOnceOnly) { - Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket) || cryptoKeyStore == null); + Contract.Requires<ArgumentException>(cryptoKeyStore != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); - if (symmetricSecret != null) { - this.symmetricHasher = new HMACSHA256(symmetricSecret); + this.cryptoKeyStore = cryptoKeyStore; + this.cryptoKeyBucket = bucket; + if (minimumAge.HasValue) { + this.minimumAge = minimumAge.Value; } - - this.symmetricSecret = symmetricSecret; } /// <summary> @@ -154,12 +162,13 @@ namespace DotNetOpenAuth.Messaging { encoded = MessagingUtilities.Compress(encoded); } + string symmetricSecretHandle = null; if (this.encrypted) { - encoded = this.Encrypt(encoded); + encoded = this.Encrypt(encoded, out symmetricSecretHandle); } if (this.signed) { - message.Signature = this.CalculateSignature(encoded); + message.Signature = this.CalculateSignature(encoded, symmetricSecretHandle); } int capacity = this.signed ? 4 + message.Signature.Length + 4 + encoded.Length : encoded.Length; @@ -172,7 +181,13 @@ namespace DotNetOpenAuth.Messaging { writer.WriteBuffer(encoded); writer.Flush(); - return Convert.ToBase64String(finalStream.ToArray()); + string payload = MessagingUtilities.ConvertToBase64WebSafeString(finalStream.ToArray()); + string result = payload; + if (symmetricSecretHandle != null && (this.signed || this.encrypted)) { + result = MessagingUtilities.CombineKeyHandleAndPayload(symmetricSecretHandle, payload); + } + + return result; } /// <summary> @@ -182,8 +197,15 @@ namespace DotNetOpenAuth.Messaging { /// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> /// <returns>The deserialized value. Never null.</returns> public T Deserialize(IProtocolMessage containingMessage, string value) { + string symmetricSecretHandle = null; + if (this.encrypted && this.cryptoKeyStore != null) { + string valueWithoutHandle; + MessagingUtilities.ExtractKeyHandleAndPayload(containingMessage, "<TODO>", value, out symmetricSecretHandle, out valueWithoutHandle); + value = valueWithoutHandle; + } + var message = new T { ContainingMessage = containingMessage }; - byte[] data = Convert.FromBase64String(value); + byte[] data = MessagingUtilities.FromBase64WebSafeString(value); byte[] signature = null; if (this.signed) { @@ -193,11 +215,11 @@ namespace DotNetOpenAuth.Messaging { data = dataReader.ReadBuffer(); // Verify that the verification code was issued by message authorization server. - ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature), MessagingStrings.SignatureInvalid); + ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature, symmetricSecretHandle), MessagingStrings.SignatureInvalid); } if (this.encrypted) { - data = this.Decrypt(data); + data = this.Decrypt(data, symmetricSecretHandle); } if (this.compressed) { @@ -249,17 +271,18 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="signedData">The signed data.</param> /// <param name="signature">The signature.</param> + /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param> /// <returns> /// <c>true</c> if the signature is valid; otherwise, <c>false</c>. /// </returns> - private bool IsSignatureValid(byte[] signedData, byte[] signature) { + private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) { Contract.Requires<ArgumentNullException>(signedData != null, "message"); Contract.Requires<ArgumentNullException>(signature != null, "signature"); if (this.asymmetricSigning != null) { return this.asymmetricSigning.VerifyData(signedData, this.hasherForAsymmetricSigning, signature); } else { - return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData)); + return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle)); } } @@ -267,18 +290,22 @@ namespace DotNetOpenAuth.Messaging { /// Calculates the signature for the data in this verification code. /// </summary> /// <param name="bytesToSign">The bytes to sign.</param> + /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param> /// <returns> /// The calculated signature. /// </returns> - private byte[] CalculateSignature(byte[] bytesToSign) { + private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) { Contract.Requires<ArgumentNullException>(bytesToSign != null, "bytesToSign"); - Contract.Requires<InvalidOperationException>(this.asymmetricSigning != null || this.symmetricHasher != null); + Contract.Requires<InvalidOperationException>(this.asymmetricSigning != null || this.cryptoKeyStore != null); Contract.Ensures(Contract.Result<byte[]>() != null); if (this.asymmetricSigning != null) { return this.asymmetricSigning.SignData(bytesToSign, this.hasherForAsymmetricSigning); } else { - return this.symmetricHasher.ComputeHash(bytesToSign); + var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle); + ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key."); + var symmetricHasher = new HMACSHA256(key.Key); + return symmetricHasher.ComputeHash(bytesToSign); } } @@ -286,14 +313,20 @@ namespace DotNetOpenAuth.Messaging { /// Encrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate. /// </summary> /// <param name="value">The value.</param> - /// <returns>The encrypted value.</returns> - private byte[] Encrypt(byte[] value) { - Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.symmetricSecret != null); + /// <param name="symmetricSecretHandle">Receives the symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param> + /// <returns> + /// The encrypted value. + /// </returns> + private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) { + Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.cryptoKeyStore != null); if (this.asymmetricEncrypting != null) { + symmetricSecretHandle = null; return this.asymmetricEncrypting.EncryptWithRandomSymmetricKey(value); } else { - return MessagingUtilities.Encrypt(value, this.symmetricSecret); + var cryptoKey = this.cryptoKeyStore.GetCurrentKey(this.cryptoKeyBucket, this.minimumAge); + symmetricSecretHandle = cryptoKey.Key; + return MessagingUtilities.Encrypt(value, cryptoKey.Value.Key); } } @@ -301,14 +334,19 @@ namespace DotNetOpenAuth.Messaging { /// Decrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate. /// </summary> /// <param name="value">The value.</param> - /// <returns>The decrypted value.</returns> - private byte[] Decrypt(byte[] value) { - Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.symmetricSecret != null); + /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param> + /// <returns> + /// The decrypted value. + /// </returns> + private byte[] Decrypt(byte[] value, string symmetricSecretHandle) { + Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || symmetricSecretHandle != null); if (this.asymmetricEncrypting != null) { return this.asymmetricEncrypting.DecryptWithRandomSymmetricKey(value); } else { - return MessagingUtilities.Decrypt(value, this.symmetricSecret); + var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle); + ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key."); + return MessagingUtilities.Decrypt(value, key.Key); } } } diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index b1807c9..8686c2e 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -29,20 +29,6 @@ namespace DotNetOpenAuth.Messaging { /// </summary> public static class MessagingUtilities { /// <summary> - /// The length of private symmetric secret handles. - /// </summary> - /// <remarks> - /// This value needn't be high, as we only expect to have a small handful of unexpired secrets at a time, - /// and handle recycling is permissible. - /// </remarks> - private const int SymmetricSecretHandleLength = 4; - - /// <summary> - /// The default lifetime of a private secret. - /// </summary> - private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.Messaging.PrivateSecretMaximumAge; - - /// <summary> /// The cryptographically strong random data generator used for creating secrets. /// </summary> /// <remarks>The random number generator is thread-safe.</remarks> @@ -91,6 +77,20 @@ namespace DotNetOpenAuth.Messaging { internal const string AlphaNumericNoLookAlikes = "23456789abcdefghjkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ"; /// <summary> + /// The length of private symmetric secret handles. + /// </summary> + /// <remarks> + /// This value needn't be high, as we only expect to have a small handful of unexpired secrets at a time, + /// and handle recycling is permissible. + /// </remarks> + private const int SymmetricSecretHandleLength = 4; + + /// <summary> + /// The default lifetime of a private secret. + /// </summary> + private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.Messaging.PrivateSecretMaximumAge; + + /// <summary> /// A character array containing just the = character. /// </summary> private static readonly char[] EqualsArray = new char[] { '=' }; @@ -427,6 +427,40 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Encodes a symmetric key handle and the blob that is encrypted/signed with that key into a single string + /// that can be decoded by <see cref="ExtractKeyHandleAndPayload"/>. + /// </summary> + /// <param name="handle">The cryptographic key handle.</param> + /// <param name="payload">The encrypted/signed blob.</param> + /// <returns>The combined encoded value.</returns> + internal static string CombineKeyHandleAndPayload(string handle, string payload) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(payload)); + Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>())); + + return handle + "!" + payload; + } + + /// <summary> + /// Extracts the key handle and encrypted blob from a string previously returned from <see cref="CombineKeyHandleAndPayload"/>. + /// </summary> + /// <param name="containingMessage">The containing message.</param> + /// <param name="messagePart">The message part.</param> + /// <param name="keyHandleAndBlob">The value previously returned from <see cref="CombineKeyHandleAndPayload"/>.</param> + /// <param name="handle">The crypto key handle.</param> + /// <param name="dataBlob">The encrypted/signed data.</param> + internal static void ExtractKeyHandleAndPayload(IProtocolMessage containingMessage, string messagePart, string keyHandleAndBlob, out string handle, out string dataBlob) { + Contract.Requires<ArgumentNullException>(containingMessage != null, "containingMessage"); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(messagePart)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(keyHandleAndBlob)); + + int privateHandleIndex = keyHandleAndBlob.IndexOf('!'); + ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, messagePart, keyHandleAndBlob); + handle = keyHandleAndBlob.Substring(0, privateHandleIndex); + dataBlob = keyHandleAndBlob.Substring(privateHandleIndex + 1); + } + + /// <summary> /// Gets a buffer of random data (not cryptographically strong). /// </summary> /// <param name="length">The length of the sequence to generate.</param> @@ -779,8 +813,20 @@ namespace DotNetOpenAuth.Messaging { 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."); + int missingPaddingCharacters; + switch (base64WebSafe.Length % 4) { + case 3: + missingPaddingCharacters = 1; + break; + case 2: + missingPaddingCharacters = 2; + break; + case 0: + missingPaddingCharacters = 0; + break; + default: + throw ErrorUtilities.ThrowInternal("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); diff --git a/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs b/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs index b435f1b..8c66128 100644 --- a/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs +++ b/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs @@ -36,15 +36,17 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Initializes a new instance of the <see cref="UriStyleMessageFormatter<T>"/> class. /// </summary> - /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param> + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param> /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param> /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param> + /// <param name="minimumAge">The minimum age.</param> /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param> /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param> - protected internal UriStyleMessageFormatter(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) - : base(symmetricSecret, signed, encrypted, compressed, maximumAge, decodeOnceOnly) { - Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); + protected internal UriStyleMessageFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) + : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) { + Contract.Requires<ArgumentException>((cryptoKeyStore != null && !String.IsNullOrEmpty(bucket)) || (!signed && !encrypted), "A secret is required when signing or encrypting is required."); } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs index a48c95a..82334ef 100644 --- a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs @@ -233,7 +233,7 @@ namespace DotNetOpenAuth.OAuth2 { response.Scope.ResetContents(tokenRequest.AuthorizationDescription.Scope); if (includeRefreshToken) { - var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServerServices.Secret); + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServerServices.CryptoKeyStore); var refreshToken = new RefreshToken(tokenRequest.AuthorizationDescription); response.RefreshToken = refreshTokenFormatter.Serialize(refreshToken); } diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs index 2404963..b772c0e 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs @@ -55,8 +55,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { var response = message as ITokenCarryingRequest; if (response != null) { - switch (response.CodeOrTokenType) - { + switch (response.CodeOrTokenType) { case CodeOrTokenType.AuthorizationCode: var codeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); var code = (AuthorizationCode)response.AuthorizationDescription; @@ -70,8 +69,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { } var accessTokenResponse = message as AccessTokenSuccessResponse; - if (accessTokenResponse != null) - { + if (accessTokenResponse != null) { var directResponseMessage = (IDirectResponseProtocolMessage)accessTokenResponse; var accessTokenRequest = (AccessTokenRequestBase)directResponseMessage.OriginatingRequest; ErrorUtilities.VerifyProtocol(accessTokenRequest.GrantType != GrantType.None || accessTokenResponse.RefreshToken == null, OAuthStrings.NoGrantNoRefreshToken); @@ -108,7 +106,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { tokenRequest.AuthorizationDescription = verificationCode; break; case CodeOrTokenType.RefreshToken: - var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.Secret); + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); var refreshToken = refreshTokenFormatter.Deserialize(message, tokenRequest.CodeOrToken); tokenRequest.AuthorizationDescription = refreshToken; break; diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs index 76867a9..6067541 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs @@ -18,6 +18,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// </summary> internal class AuthorizationCode : AuthorizationDataBag { /// <summary> + /// The name of the bucket for symmetric keys used to sign authorization codes. + /// </summary> + internal const string AuthorizationCodeKeyBucket = "https://localhost/dnoa/oauth_authorization_code"; + + /// <summary> /// The hash algorithm used on the callback URI. /// </summary> private readonly HashAlgorithm hasher = new SHA256Managed(); @@ -61,12 +66,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { Contract.Ensures(Contract.Result<IDataBagFormatter<AuthorizationCode>>() != null); return new UriStyleMessageFormatter<AuthorizationCode>( - authorizationServer.Secret, - true, - true, - false, - AuthorizationCodeBindingElement.MaximumMessageAge, - authorizationServer.VerificationCodeNonceStore); + authorizationServer.CryptoKeyStore, + AuthorizationCodeKeyBucket, + signed: true, + encrypted: true, + compressed: false, + maximumAge: AuthorizationCodeBindingElement.MaximumMessageAge, + decodeOnceOnly: authorizationServer.VerificationCodeNonceStore); } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs index 8feb3fb..4662719 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { using System; using System.Diagnostics.Contracts; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; /// <summary> /// The refresh token issued to a client by an authorization server that allows the client @@ -15,6 +16,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// </summary> internal class RefreshToken : AuthorizationDataBag { /// <summary> + /// The name of the bucket for symmetric keys used to sign refresh tokens. + /// </summary> + internal const string RefreshTokenKeyBucket = "https://localhost/dnoa/oauth_refresh_token"; + + /// <summary> /// Initializes a new instance of the <see cref="RefreshToken"/> class. /// </summary> public RefreshToken() { @@ -36,14 +42,15 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// Creates a formatter capable of serializing/deserializing a refresh token. /// </summary> - /// <param name="symmetricSecret">The symmetric secret used by the authorization server to sign/encrypt refresh tokens. Must not be null.</param> - /// <returns>A DataBag formatter. Never null.</returns> - internal static IDataBagFormatter<RefreshToken> CreateFormatter(byte[] symmetricSecret) - { - Contract.Requires<ArgumentNullException>(symmetricSecret != null, "symmetricSecret"); + /// <param name="cryptoKeyStore">The crypto key store.</param> + /// <returns> + /// A DataBag formatter. Never null. + /// </returns> + internal static IDataBagFormatter<RefreshToken> CreateFormatter(ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore"); Contract.Ensures(Contract.Result<IDataBagFormatter<RefreshToken>>() != null); - return new UriStyleMessageFormatter<RefreshToken>(symmetricSecret, true, true); + return new UriStyleMessageFormatter<RefreshToken>(cryptoKeyStore, RefreshTokenKeyBucket, signed: true, encrypted: true); } } } diff --git a/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs index 9a62277..d35373b 100644 --- a/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs +++ b/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs @@ -20,14 +20,14 @@ namespace DotNetOpenAuth.OAuth2 { [ContractClass(typeof(IAuthorizationServerContract))] public interface IAuthorizationServer { /// <summary> - /// Gets the secret used to symmetrically encrypt and sign authorization codes and refresh tokens. + /// Gets the store for storeing crypto keys used to symmetrically encrypt and sign authorization codes and refresh tokens. /// </summary> /// <remarks> - /// This secret should be kept strictly confidential in the authorization server(s) - /// and NOT shared with the resource server. Anyone with this secret can mint + /// This store should be kept strictly confidential in the authorization server(s) + /// and NOT shared with the resource server. Anyone with these secrets can mint /// tokens to essentially grant themselves access to anything they want. /// </remarks> - byte[] Secret { get; } + ICryptoKeyStore CryptoKeyStore { get; } /// <summary> /// Gets the authorization code nonce store to use to ensure that authorization codes can only be used once. @@ -92,17 +92,11 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> - /// Gets the secret used to symmetrically encrypt and sign authorization codes and refresh tokens. + /// Gets the store for storeing crypto keys used to symmetrically encrypt and sign authorization codes and refresh tokens. /// </summary> - /// <value></value> - /// <remarks> - /// This secret should be kept strictly confidential in the authorization server(s) - /// and NOT shared with the resource server. Anyone with this secret can mint - /// tokens to essentially grant themselves access to anything they want. - /// </remarks> - byte[] IAuthorizationServer.Secret { + ICryptoKeyStore IAuthorizationServer.CryptoKeyStore { get { - Contract.Ensures(Contract.Result<byte[]>() != null); + Contract.Ensures(Contract.Result<ICryptoKeyStore>() != null); throw new NotImplementedException(); } } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs index 702e947..2e95436 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -164,7 +164,10 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// Gets the return to signature. /// </summary> /// <param name="returnTo">The return to.</param> - /// <returns>The generated signature.</returns> + /// <param name="cryptoKey">The crypto key.</param> + /// <returns> + /// The generated signature. + /// </returns> /// <remarks> /// Only the parameters in the return_to URI are signed, rather than the base URI /// itself, in order that OPs that might change the return_to's implicit port :80 part diff --git a/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs b/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs index 3caf05e..72949d9 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs @@ -7,10 +7,12 @@ namespace DotNetOpenAuth.OpenId.Provider { using System; using System.Collections.Generic; + using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; /// <summary> /// A signed and encrypted serialization of an association. @@ -77,10 +79,17 @@ namespace DotNetOpenAuth.OpenId.Provider { /// <summary> /// Creates the formatter used for serialization of this type. /// </summary> - /// <param name="symmetricSecret">The OpenID Provider's private symmetric secret to use to encrypt and sign the association data.</param> - /// <returns>A formatter for serialization.</returns> - internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(byte[] symmetricSecret) { - return new BinaryDataBagFormatter<AssociationDataBag>(symmetricSecret, signed: true, encrypted: true); + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> + /// <param name="minimumAge">The minimum age.</param> + /// <returns> + /// A formatter for serialization. + /// </returns> + internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan? minimumAge = null) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore"); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket)); + Contract.Ensures(Contract.Result<IDataBagFormatter<AssociationDataBag>>() != null); + return new BinaryDataBagFormatter<AssociationDataBag>(cryptoKeyStore, bucket, signed: true, encrypted: true, minimumAge: minimumAge); } } } diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs index e8c8881..4a52728 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs @@ -74,6 +74,7 @@ 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> + /// <param name="cryptoKeyStore">The crypto key store. Cannot be null.</param> private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) { Contract.Requires<ArgumentNullException>(nonceStore != null, "nonceStore"); Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "associationStore"); diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs index 358daf4..73d8b56 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs @@ -17,13 +17,20 @@ namespace DotNetOpenAuth.OpenId.Provider { /// details in the handle. /// </summary> public class ProviderAssociationHandleEncoder : IProviderAssociationStore { + /// <summary> + /// The name of the bucket in which to store keys that encrypt association data into association handles. + /// </summary> internal const string AssociationHandleEncodingSecretBucket = "https://localhost/dnoa/association_handles"; + /// <summary> + /// The crypto key store used to persist encryption keys. + /// </summary> private readonly ICryptoKeyStore cryptoKeyStore; /// <summary> /// Initializes a new instance of the <see cref="ProviderAssociationHandleEncoder"/> class. /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> public ProviderAssociationHandleEncoder(ICryptoKeyStore cryptoKeyStore) { Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore"); this.cryptoKeyStore = cryptoKeyStore; @@ -45,9 +52,8 @@ namespace DotNetOpenAuth.OpenId.Provider { ExpiresUtc = expiresUtc, }; - var encodingSecret = this.cryptoKeyStore.GetCurrentKey(AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow); - var formatter = AssociationDataBag.CreateFormatter(encodingSecret.Value.Key); - return encodingSecret.Key + "!" + formatter.Serialize(associationDataBag); + var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow); + return formatter.Serialize(associationDataBag); } /// <summary> @@ -61,21 +67,12 @@ 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) { - 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); + var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket); AssociationDataBag bag; try { - bag = formatter.Deserialize(containingMessage, encodedHandle); - } catch (ProtocolException) { + bag = formatter.Deserialize(containingMessage, handle); + } catch (ProtocolException ex) { + Logger.OpenId.Error("Rejecting an association because deserialization of the encoded handle failed.", ex); return null; } diff --git a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs index 265b555..c13c4bc 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OpenId.Provider { using System; + using System.Collections.Generic; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging.Bindings; @@ -27,6 +28,9 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </summary> private readonly INonceStore nonceStore; + /// <summary> + /// The crypto key store where symmetric keys are persisted. + /// </summary> private readonly ICryptoKeyStore cryptoKeyStore; /// <summary> @@ -65,18 +69,45 @@ namespace DotNetOpenAuth.OpenId.Provider { #region ICryptoKeyStore + /// <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) { return this.cryptoKeyStore.GetKey(bucket, handle); } - public System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, CryptoKey>> GetKeys(string bucket) { + /// <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) { return this.cryptoKeyStore.GetKeys(bucket); } + /// <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) { this.cryptoKeyStore.StoreKey(bucket, handle, key); } + /// <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) { this.cryptoKeyStore.RemoveKey(bucket, handle); } |