diff options
28 files changed, 501 insertions, 181 deletions
diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs index b1f9787..f0608d5 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs @@ -9,9 +9,9 @@ namespace RelyingPartyLogic { using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; using System.Text; using System.Web; - using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth2; using DotNetOpenAuth.OAuth2.ChannelElements; @@ -23,30 +23,18 @@ namespace RelyingPartyLogic { public class OAuthAuthorizationServer : IAuthorizationServer { private static readonly RSAParameters AsymmetricKey = CreateRSAKey(); - private static readonly byte[] secret = CreateSecret(); - private readonly INonceStore nonceStore = new NonceDbStore(); /// <summary> /// Initializes a new instance of the <see cref="OAuthAuthorizationServer"/> class. /// </summary> public OAuthAuthorizationServer() { + this.CryptoKeyStore = new RelyingPartyApplicationDbStore(); } #region IAuthorizationServer Members - /// <summary> - /// Gets the secret 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> - public byte[] Secret { - get { return secret; } - } + public ICryptoKeyStore CryptoKeyStore { get; private set; } /// <summary> /// Gets the authorization code nonce store to use to ensure that authorization codes can only be used once. @@ -142,19 +130,6 @@ namespace RelyingPartyLogic { } /// <summary> - /// Creates a symmetric secret used to sign and encrypt authorization server refresh tokens. - /// </summary> - /// <returns>A cryptographically strong symmetric key.</returns> - private static byte[] CreateSecret() { - // TODO: Replace this sample code with real code. - // For this sample, we just generate random secrets. - RandomNumberGenerator crypto = new RNGCryptoServiceProvider(); - var secret = new byte[16]; - crypto.GetBytes(secret); - return secret; - } - - /// <summary> /// Creates the RSA key used by all the crypto service provider instances we create. /// </summary> /// <returns>RSA data that includes the private key.</returns> diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml index 33e6eda..0ef987d 100644 --- a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml +++ b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml @@ -37,4 +37,12 @@ <Column Name="Timestamp" Type="System.DateTime" IsPrimaryKey="true" CanBeNull="false" /> </Type> </Table> + <Table Name="" Member="SymmetricCryptoKeys"> + <Type Name="SymmetricCryptoKey"> + <Column Name="Bucket" Type="System.String" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" /> + <Column Name="Handle" Type="System.String" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" /> + <Column Name="ExpiresUtc" Type="System.DateTime" CanBeNull="false" UpdateCheck="Never" /> + <Column Name="Secret" Type="System.Byte[]" CanBeNull="false" UpdateCheck="Never" /> + </Type> + </Table> </Database>
\ No newline at end of file diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout index e2982ce..f4de725 100644 --- a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout +++ b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout @@ -40,5 +40,11 @@ <classShapeMoniker Id="895ebbc8-8352-4c04-9e53-b8e6c8302d36" /> </nodes> </associationConnector> + <classShape Id="93df6fa9-cc66-44a9-8885-960b1e670dd7" absoluteBounds="4.125, 6.25, 2, 1.5785953776041666"> + <DataClassMoniker Name="/DataClassesDataContext/SymmetricCryptoKey" /> + <nestedChildShapes> + <elementListCompartment Id="0b486eb8-31a4-4f11-b58f-09540c56319b" absoluteBounds="4.14, 6.71, 1.9700000000000002, 1.0185953776041665" name="DataPropertiesCompartment" titleTextColor="Black" itemTextColor="Black" /> + </nestedChildShapes> + </classShape> </nestedChildShapes> </ordesignerObjectsDiagram>
\ No newline at end of file diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs b/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs index b6d070d..c8d1b19 100644 --- a/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs +++ b/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs @@ -2,7 +2,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.1 +// Runtime Version:4.0.30319.225 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -42,6 +42,9 @@ namespace OAuthAuthorizationServer.Code partial void InsertNonce(Nonce instance); partial void UpdateNonce(Nonce instance); partial void DeleteNonce(Nonce instance); + partial void InsertSymmetricCryptoKey(SymmetricCryptoKey instance); + partial void UpdateSymmetricCryptoKey(SymmetricCryptoKey instance); + partial void DeleteSymmetricCryptoKey(SymmetricCryptoKey instance); #endregion public DataClassesDataContext() : @@ -105,6 +108,14 @@ namespace OAuthAuthorizationServer.Code return this.GetTable<Nonce>(); } } + + public System.Data.Linq.Table<SymmetricCryptoKey> SymmetricCryptoKeys + { + get + { + return this.GetTable<SymmetricCryptoKey>(); + } + } } [global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.[User]")] @@ -804,5 +815,139 @@ namespace OAuthAuthorizationServer.Code } } } + + [global::System.Data.Linq.Mapping.TableAttribute(Name="")] + public partial class SymmetricCryptoKey : INotifyPropertyChanging, INotifyPropertyChanged + { + + private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); + + private string _Bucket; + + private string _Handle; + + private System.DateTime _ExpiresUtc; + + private byte[] _Secret; + + #region Extensibility Method Definitions + partial void OnLoaded(); + partial void OnValidate(System.Data.Linq.ChangeAction action); + partial void OnCreated(); + partial void OnBucketChanging(string value); + partial void OnBucketChanged(); + partial void OnHandleChanging(string value); + partial void OnHandleChanged(); + partial void OnExpiresUtcChanging(System.DateTime value); + partial void OnExpiresUtcChanged(); + partial void OnSecretChanging(byte[] value); + partial void OnSecretChanged(); + #endregion + + public SymmetricCryptoKey() + { + OnCreated(); + } + + [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Bucket", CanBeNull=false, IsPrimaryKey=true, UpdateCheck=UpdateCheck.Never)] + public string Bucket + { + get + { + return this._Bucket; + } + set + { + if ((this._Bucket != value)) + { + this.OnBucketChanging(value); + this.SendPropertyChanging(); + this._Bucket = value; + this.SendPropertyChanged("Bucket"); + this.OnBucketChanged(); + } + } + } + + [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Handle", CanBeNull=false, IsPrimaryKey=true, UpdateCheck=UpdateCheck.Never)] + public string Handle + { + get + { + return this._Handle; + } + set + { + if ((this._Handle != value)) + { + this.OnHandleChanging(value); + this.SendPropertyChanging(); + this._Handle = value; + this.SendPropertyChanged("Handle"); + this.OnHandleChanged(); + } + } + } + + [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ExpiresUtc", UpdateCheck=UpdateCheck.Never)] + public System.DateTime ExpiresUtc + { + get + { + return this._ExpiresUtc; + } + set + { + if ((this._ExpiresUtc != value)) + { + this.OnExpiresUtcChanging(value); + this.SendPropertyChanging(); + this._ExpiresUtc = value; + this.SendPropertyChanged("ExpiresUtc"); + this.OnExpiresUtcChanged(); + } + } + } + + [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Secret", CanBeNull=false, UpdateCheck=UpdateCheck.Never)] + public byte[] Secret + { + get + { + return this._Secret; + } + set + { + if ((this._Secret != value)) + { + this.OnSecretChanging(value); + this.SendPropertyChanging(); + this._Secret = value; + this.SendPropertyChanged("Secret"); + this.OnSecretChanged(); + } + } + } + + public event PropertyChangingEventHandler PropertyChanging; + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void SendPropertyChanging() + { + if ((this.PropertyChanging != null)) + { + this.PropertyChanging(this, emptyChangingEventArgs); + } + } + + protected virtual void SendPropertyChanged(String propertyName) + { + if ((this.PropertyChanged != null)) + { + this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + } + } } #pragma warning restore 1591 diff --git a/samples/OAuthAuthorizationServer/Code/DatabaseNonceStore.cs b/samples/OAuthAuthorizationServer/Code/DatabaseKeyNonceStore.cs index a0ce19e..765696e 100644 --- a/samples/OAuthAuthorizationServer/Code/DatabaseNonceStore.cs +++ b/samples/OAuthAuthorizationServer/Code/DatabaseKeyNonceStore.cs @@ -1,16 +1,18 @@ namespace OAuthAuthorizationServer.Code { using System; + using System.Collections.Generic; using System.Data.SqlClient; + using System.Linq; using DotNetOpenAuth.Messaging.Bindings; /// <summary> /// A database-persisted nonce store. /// </summary> - public class DatabaseNonceStore : INonceStore { + public class DatabaseKeyNonceStore : INonceStore, ICryptoKeyStore { /// <summary> - /// Initializes a new instance of the <see cref="DatabaseNonceStore"/> class. + /// Initializes a new instance of the <see cref="DatabaseKeyNonceStore"/> class. /// </summary> - public DatabaseNonceStore() { + public DatabaseKeyNonceStore() { } #region INonceStore Members @@ -51,5 +53,43 @@ } #endregion + + #region ICryptoKeyStore Members + + public CryptoKey GetKey(string bucket, string handle) { + // It is critical that this lookup be case-sensitive, which can only be configured at the database. + var matches = from key in MvcApplication.DataContext.SymmetricCryptoKeys + where key.Bucket == bucket && key.Handle == handle + select new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc()); + + return matches.FirstOrDefault(); + } + + public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) { + return from key in MvcApplication.DataContext.SymmetricCryptoKeys + orderby key.ExpiresUtc descending + select new KeyValuePair<string, CryptoKey>(key.Handle, new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc())); + } + + public void StoreKey(string bucket, string handle, CryptoKey key) { + var keyRow = new SymmetricCryptoKey() { + Bucket = bucket, + Handle = handle, + Secret = key.Key, + ExpiresUtc = key.ExpiresUtc, + }; + + MvcApplication.DataContext.SymmetricCryptoKeys.InsertOnSubmit(keyRow); + MvcApplication.DataContext.SubmitChanges(); + } + + public void RemoveKey(string bucket, string handle) { + var match = MvcApplication.DataContext.SymmetricCryptoKeys.FirstOrDefault(k => k.Bucket == bucket && k.Handle == handle); + if (match != null) { + MvcApplication.DataContext.SymmetricCryptoKeys.DeleteOnSubmit(match); + } + } + + #endregion } }
\ No newline at end of file diff --git a/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs b/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs index 3be70f0..90f99f8 100644 --- a/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs +++ b/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs @@ -13,18 +13,14 @@ internal class OAuth2AuthorizationServer : IAuthorizationServer { private static readonly RSAParameters AsymmetricTokenSigningPrivateKey = CreateRSAKey(); - private static readonly byte[] secret = CreateSecret(); - - private readonly INonceStore nonceStore = new DatabaseNonceStore(); - #region Implementation of IAuthorizationServer - public byte[] Secret { - get { return secret; } + public ICryptoKeyStore CryptoKeyStore { + get { return MvcApplication.KeyNonceStore; } } public INonceStore VerificationCodeNonceStore { - get { return this.nonceStore; } + get { return MvcApplication.KeyNonceStore; } } public RSACryptoServiceProvider CreateAccessTokenSigningCryptoServiceProvider() { @@ -75,19 +71,6 @@ } /// <summary> - /// Creates a symmetric secret used to sign and encrypt authorization server refresh tokens. - /// </summary> - /// <returns>A cryptographically strong symmetric key.</returns> - private static byte[] CreateSecret() { - // TODO: Replace this sample code with real code. - // For this sample, we just generate random secrets. - RandomNumberGenerator crypto = new RNGCryptoServiceProvider(); - var secret = new byte[32]; // 256-bit symmetric key to protect all protected resources. - crypto.GetBytes(secret); - return secret; - } - - /// <summary> /// Creates the RSA key used by all the crypto service provider instances we create. /// </summary> /// <returns>RSA data that includes the private key.</returns> @@ -126,12 +109,12 @@ private bool IsAuthorizationValid(HashSet<string> requestedScopes, string clientIdentifier, DateTime issuedUtc, string username) { var grantedScopeStrings = from auth in MvcApplication.DataContext.ClientAuthorizations - where - auth.Client.ClientIdentifier == clientIdentifier && - auth.CreatedOnUtc <= issuedUtc && - (!auth.ExpirationDateUtc.HasValue || auth.ExpirationDateUtc.Value >= DateTime.UtcNow) && - auth.User.OpenIDClaimedIdentifier == username - select auth.Scope; + where + auth.Client.ClientIdentifier == clientIdentifier && + auth.CreatedOnUtc <= issuedUtc && + (!auth.ExpirationDateUtc.HasValue || auth.ExpirationDateUtc.Value >= DateTime.UtcNow) && + auth.User.OpenIDClaimedIdentifier == username + select auth.Scope; if (!grantedScopeStrings.Any()) { // No granted authorizations prior to the issuance of this token, so it must have been revoked. diff --git a/samples/OAuthAuthorizationServer/Code/Utilities.cs b/samples/OAuthAuthorizationServer/Code/Utilities.cs new file mode 100644 index 0000000..c9109bd --- /dev/null +++ b/samples/OAuthAuthorizationServer/Code/Utilities.cs @@ -0,0 +1,21 @@ +namespace OAuthAuthorizationServer.Code { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web; + + internal static class Utilities { + /// <summary> + /// Ensures that local times are converted to UTC times. Unspecified kinds are recast to UTC with no conversion. + /// </summary> + /// <param name="value">The date-time to convert.</param> + /// <returns>The date-time in UTC time.</returns> + internal static DateTime AsUtc(this DateTime value) { + if (value.Kind == DateTimeKind.Unspecified) { + return new DateTime(value.Ticks, DateTimeKind.Utc); + } + + return value.ToUniversalTime(); + } + } +}
\ No newline at end of file diff --git a/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs b/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs index 47c1977..11e7b11 100644 --- a/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs +++ b/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs @@ -35,16 +35,6 @@ #endif
/// <summary>
- /// Creates the resource server's encryption service provider with private key.
- /// </summary>
- /// <returns>An RSA crypto service provider.</returns>
- internal static RSACryptoServiceProvider CreateResourceServerEncryptionServiceProvider() {
- var resourceServerEncryptionServiceProvider = new RSACryptoServiceProvider();
- resourceServerEncryptionServiceProvider.ImportParameters(ResourceServerEncryptionPublicKey);
- return resourceServerEncryptionServiceProvider;
- }
-
- /// <summary>
/// The OAuth 2.0 token endpoint.
/// </summary>
/// <returns>The response to the Client.</returns>
@@ -133,5 +123,15 @@ return this.authorizationServer.Channel.PrepareResponse(response).AsActionResult();
}
+
+ /// <summary>
+ /// Creates the resource server's encryption service provider with private key.
+ /// </summary>
+ /// <returns>An RSA crypto service provider.</returns>
+ internal static RSACryptoServiceProvider CreateResourceServerEncryptionServiceProvider() {
+ var resourceServerEncryptionServiceProvider = new RSACryptoServiceProvider();
+ resourceServerEncryptionServiceProvider.ImportParameters(ResourceServerEncryptionPublicKey);
+ return resourceServerEncryptionServiceProvider;
+ }
}
}
diff --git a/samples/OAuthAuthorizationServer/Global.asax.cs b/samples/OAuthAuthorizationServer/Global.asax.cs index 2c23ec0..d878ea6 100644 --- a/samples/OAuthAuthorizationServer/Global.asax.cs +++ b/samples/OAuthAuthorizationServer/Global.asax.cs @@ -26,7 +26,7 @@ /// </summary>
public static log4net.ILog Logger = log4net.LogManager.GetLogger("DotNetOpenAuth.OAuthAuthorizationServer");
- public static DatabaseNonceStore NonceStore { get; set; }
+ public static DatabaseKeyNonceStore KeyNonceStore { get; set; }
/// <summary>
/// Gets the transaction-protected database connection for the current request.
@@ -81,7 +81,7 @@ RegisterRoutes(RouteTable.Routes);
- NonceStore = new DatabaseNonceStore();
+ KeyNonceStore = new DatabaseKeyNonceStore();
log4net.Config.XmlConfigurator.Configure();
Logger.Info("Sample starting...");
diff --git a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj index 5bb1daf..d00cef4 100644 --- a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj +++ b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj @@ -70,13 +70,14 @@ </ItemGroup> <ItemGroup> <Compile Include="Code\Client.cs" /> - <Compile Include="Code\DatabaseNonceStore.cs" /> + <Compile Include="Code\DatabaseKeyNonceStore.cs" /> <Compile Include="Code\DataClasses.designer.cs"> <DependentUpon>DataClasses.dbml</DependentUpon> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> </Compile> <Compile Include="Code\OAuth2AuthorizationServer.cs" /> + <Compile Include="Code\Utilities.cs" /> <Compile Include="Controllers\AccountController.cs" /> <Compile Include="Controllers\HomeController.cs" /> <Compile Include="Controllers\OAuthController.cs" /> 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); } |