diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2008-09-12 07:58:09 -0700 |
---|---|---|
committer | Andrew <andrewarnott@gmail.com> | 2008-09-12 07:58:09 -0700 |
commit | 6c4936e194080e6a7d2194870cf57814d6432eff (patch) | |
tree | aafb28006cb0aa9e9125cce940967e051c12a53d /src | |
parent | 608bdfba197391ddd98acdf1a4a75062e5f431e5 (diff) | |
download | DotNetOpenAuth-6c4936e194080e6a7d2194870cf57814d6432eff.zip DotNetOpenAuth-6c4936e194080e6a7d2194870cf57814d6432eff.tar.gz DotNetOpenAuth-6c4936e194080e6a7d2194870cf57814d6432eff.tar.bz2 |
Added expiring messages and replay protection support infrastructure.
Diffstat (limited to 'src')
-rw-r--r-- | src/DotNetOAuth/DotNetOAuth.csproj | 2 | ||||
-rw-r--r-- | src/DotNetOAuth/Messaging/Channel.cs | 145 | ||||
-rw-r--r-- | src/DotNetOAuth/Messaging/IExpiringProtocolMessage.cs | 29 | ||||
-rw-r--r-- | src/DotNetOAuth/Messaging/IReplayProtectedProtocolMessage.cs | 23 | ||||
-rw-r--r-- | src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs | 27 | ||||
-rw-r--r-- | src/DotNetOAuth/Messaging/MessagingStrings.resx | 9 |
6 files changed, 220 insertions, 15 deletions
diff --git a/src/DotNetOAuth/DotNetOAuth.csproj b/src/DotNetOAuth/DotNetOAuth.csproj index fbaa90e..dfe95b7 100644 --- a/src/DotNetOAuth/DotNetOAuth.csproj +++ b/src/DotNetOAuth/DotNetOAuth.csproj @@ -68,6 +68,8 @@ <ItemGroup>
<Compile Include="Consumer.cs" />
<Compile Include="IWebRequestHandler.cs" />
+ <Compile Include="Messaging\IReplayProtectedProtocolMessage.cs" />
+ <Compile Include="Messaging\IExpiringProtocolMessage.cs" />
<Compile Include="Messaging\DictionaryXmlReader.cs" />
<Compile Include="Messaging\DictionaryXmlWriter.cs" />
<Compile Include="Messaging\Channel.cs" />
diff --git a/src/DotNetOAuth/Messaging/Channel.cs b/src/DotNetOAuth/Messaging/Channel.cs index 9ade2de..f26a9eb 100644 --- a/src/DotNetOAuth/Messaging/Channel.cs +++ b/src/DotNetOAuth/Messaging/Channel.cs @@ -68,10 +68,27 @@ namespace DotNetOAuth.Messaging { throw new ArgumentNullException("messageTypeProvider");
}
+ this.MaximumMessageAge = TimeSpan.FromMinutes(13);
this.messageTypeProvider = messageTypeProvider;
}
/// <summary>
+ /// Gets or sets the maximum age a message implementing the
+ /// <see cref="IExpiringProtocolMessage"/> interface can be before
+ /// being discarded as too old.
+ /// </summary>
+ /// <value>The default value is 13 minutes.</value>
+ /// <remarks>
+ /// This time limit should take into account expected time skew for servers
+ /// across the Internet. For example, if a server could conceivably have its
+ /// clock d = 5 minutes off UTC time, then any two servers could have
+ /// their clocks disagree by as much as 2*d = 10 minutes.
+ /// If a message should live for at least t = 3 minutes,
+ /// this property should be set to (2*d + t) = 13 minutes.
+ /// </remarks>
+ protected internal TimeSpan MaximumMessageAge { get; set; }
+
+ /// <summary>
/// Gets a tool that can figure out what kind of message is being received
/// so it can be deserialized.
/// </summary>
@@ -98,7 +115,7 @@ namespace DotNetOAuth.Messaging { if (message == null) {
throw new ArgumentNullException("message");
}
- this.SignIfApplicable(message);
+ this.PrepareMessageForSending(message);
switch (message.Transport) {
case MessageTransport.Direct:
@@ -154,7 +171,7 @@ namespace DotNetOAuth.Messaging { /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
protected internal IProtocolMessage ReadFromRequest(HttpRequestInfo httpRequest) {
IProtocolMessage requestMessage = this.ReadFromRequestInternal(httpRequest);
- this.VerifySignatureIfApplicable(requestMessage);
+ this.VerifyMessageAfterReceiving(requestMessage);
return requestMessage;
}
@@ -164,9 +181,9 @@ namespace DotNetOAuth.Messaging { /// <param name="request">The message to send.</param>
/// <returns>The remote party's response.</returns>
protected internal IProtocolMessage Request(IDirectedProtocolMessage request) {
- this.SignIfApplicable(request);
+ this.PrepareMessageForSending(request);
IProtocolMessage response = this.RequestInternal(request);
- this.VerifySignatureIfApplicable(response);
+ this.VerifyMessageAfterReceiving(response);
return response;
}
@@ -177,7 +194,7 @@ namespace DotNetOAuth.Messaging { /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
protected internal IProtocolMessage ReadFromResponse(Stream responseStream) {
IProtocolMessage message = this.ReadFromResponseInternal(responseStream);
- this.VerifySignatureIfApplicable(message);
+ this.VerifyMessageAfterReceiving(message);
return message;
}
@@ -341,6 +358,36 @@ namespace DotNetOAuth.Messaging { }
/// <summary>
+ /// Applies replay protection on an outgoing message.
+ /// </summary>
+ /// <param name="message">The message to apply replay protection to.</param>
+ /// <remarks>
+ /// <para>Implementing this method typically involves generating and setting a nonce property
+ /// on the message.</para>
+ /// <para>
+ /// At the time this method is called, the
+ /// <see cref="IExpiringProtocolMessage.UtcCreationDate"/> property will already be
+ /// set on the <paramref name="message"/>.</para>
+ /// </remarks>
+ protected virtual void ApplyReplayProtection(IReplayProtectedProtocolMessage message) {
+ throw new NotSupportedException(MessagingStrings.ReplayProtectionNotSupported);
+ }
+
+ /// <summary>
+ /// Gets whether this message has already been processed based on the
+ /// replay protection applied by <see cref="ApplyReplayProtection"/>.
+ /// </summary>
+ /// <param name="message">The message to be checked against the list of recently received messages.</param>
+ /// <returns>True if the message has already been processed. False otherwise.</returns>
+ /// <remarks>
+ /// An exception should NOT be thrown by this method in case of a message replay.
+ /// The caller will be responsible to handle the replay attack.
+ /// </remarks>
+ protected virtual bool IsMessageReplayed(IReplayProtectedProtocolMessage message) {
+ throw new NotSupportedException(MessagingStrings.ReplayProtectionNotSupported);
+ }
+
+ /// <summary>
/// Gets the protocol message that may be in the given HTTP response stream.
/// </summary>
/// <param name="responseStream">The response that is anticipated to contain an OAuth message.</param>
@@ -399,29 +446,97 @@ namespace DotNetOAuth.Messaging { }
/// <summary>
- /// Signs a given message if the message requires one.
+ /// Prepares a message for transmit by applying signatures, nonces, etc.
/// </summary>
- /// <param name="message">The message to sign.</param>
- private void SignIfApplicable(IProtocolMessage message) {
+ /// <param name="message">The message to prepare for sending.</param>
+ private void PrepareMessageForSending(IProtocolMessage message) {
+ // The order of operations here is important.
ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
if (signedMessage != null) {
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
+ if (nonceMessage != null) {
+ this.ApplyReplayProtection(nonceMessage);
+ }
+
+ expiringMessage.UtcCreationDate = DateTime.UtcNow;
+ }
+
this.Sign(signedMessage);
}
}
/// <summary>
- /// Verifies that a given message has a valid signature if the message requires one.
+ /// Verifies the integrity and applicability of an incoming message.
/// </summary>
- /// <param name="message">The message to verify the signature on.</param>
- /// <exception cref="ProtocolException">Thrown when the signature is invalid.</exception>
- private void VerifySignatureIfApplicable(IProtocolMessage message) {
+ /// <param name="message">The message just received.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the message is somehow invalid.
+ /// This can be due to tampering, replay attack or expiration, among other things.
+ /// </exception>
+ private void VerifyMessageAfterReceiving(IProtocolMessage message) {
+ // The order of operations is important.
ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
if (signedMessage != null) {
- if (!this.IsSignatureValid(signedMessage)) {
- // TODO: add inResponseTo and remoteReceiver where applicable
- throw new ProtocolException(MessagingStrings.SignatureInvalid);
+ this.VerifyMessageSignature(signedMessage);
+
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ this.VerifyMessageHasNotExpired(expiringMessage);
+
+ IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
+ if (nonceMessage != null) {
+ this.VerifyMessageReplayProtection(nonceMessage);
+ }
}
}
}
+
+ /// <summary>
+ /// Verifies that a message signature is valid.
+ /// </summary>
+ /// <param name="signedMessage">The message whose signature is to be verified.</param>
+ /// <exception cref="ProtocolException">Thrown if the signature is invalid.</exception>
+ private void VerifyMessageSignature(ISignedProtocolMessage signedMessage) {
+ Debug.Assert(signedMessage != null, "signedMessage == null");
+
+ if (!this.IsSignatureValid(signedMessage)) {
+ // TODO: add inResponseTo and remoteReceiver where applicable
+ throw new ProtocolException(MessagingStrings.SignatureInvalid);
+ }
+ }
+
+ /// <summary>
+ /// Verifies that a given message has not grown too old to process.
+ /// </summary>
+ /// <param name="expiringMessage">The message to ensure has not expired.</param>
+ /// <exception cref="ProtocolException">Thrown if the message has already expired.</exception>
+ private void VerifyMessageHasNotExpired(IExpiringProtocolMessage expiringMessage) {
+ Debug.Assert(expiringMessage != null, "expiringMessage == null");
+
+ // Yes the UtcCreationDate is supposed to always be in UTC already,
+ // but just in case a given message failed to guarantee that, we do it here.
+ DateTime expirationDate = expiringMessage.UtcCreationDate.ToUniversalTime() + this.MaximumMessageAge;
+ if (expirationDate < DateTime.UtcNow) {
+ throw new ProtocolException(string.Format(
+ MessagingStrings.ExpiredMessage,
+ expirationDate,
+ DateTime.UtcNow));
+ }
+ }
+
+ /// <summary>
+ /// Verifies that a message has not already been processed.
+ /// </summary>
+ /// <param name="message">The message to verify.</param>
+ /// <exception cref="ProtocolException">Thrown if the message has already been processed.</exception>
+ private void VerifyMessageReplayProtection(IReplayProtectedProtocolMessage message) {
+ Debug.Assert(message != null, "message == null");
+
+ if (this.IsMessageReplayed(message)) {
+ throw new ProtocolException(MessagingStrings.ReplayAttackDetected);
+ }
+ }
}
}
diff --git a/src/DotNetOAuth/Messaging/IExpiringProtocolMessage.cs b/src/DotNetOAuth/Messaging/IExpiringProtocolMessage.cs new file mode 100644 index 0000000..3867369 --- /dev/null +++ b/src/DotNetOAuth/Messaging/IExpiringProtocolMessage.cs @@ -0,0 +1,29 @@ +//-----------------------------------------------------------------------
+// <copyright file="IExpiringProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// The contract a message that has an allowable time window for processing must implement.
+ /// </summary>
+ /// <remarks>
+ /// All expiring messages must also be signed to prevent tampering with the creation date.
+ /// </remarks>
+ internal interface IExpiringProtocolMessage : ISignedProtocolMessage {
+ /// <summary>
+ /// Gets or sets the UTC date/time the message was originally sent onto the network.
+ /// </summary>
+ /// <remarks>
+ /// The property setter should ensure a UTC date/time,
+ /// and throw an exception if this is not possible.
+ /// </remarks>
+ /// <exception cref="ArgumentException">
+ /// Thrown when a DateTime that cannot be converted to UTC is set.
+ /// </exception>
+ DateTime UtcCreationDate { get; set; }
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/IReplayProtectedProtocolMessage.cs b/src/DotNetOAuth/Messaging/IReplayProtectedProtocolMessage.cs new file mode 100644 index 0000000..bc6cd7b --- /dev/null +++ b/src/DotNetOAuth/Messaging/IReplayProtectedProtocolMessage.cs @@ -0,0 +1,23 @@ +//-----------------------------------------------------------------------
+// <copyright file="IReplayProtectedProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// The contract a message that has an allowable time window for processing must implement.
+ /// </summary>
+ /// <remarks>
+ /// All replay-protected messages must also be set to expire so the nonces do not have
+ /// to be stored indefinitely.
+ /// </remarks>
+ internal interface IReplayProtectedProtocolMessage : IExpiringProtocolMessage {
+ /// <summary>
+ /// Gets or sets the nonce that will protect the message from replay attacks.
+ /// </summary>
+ string Nonce { get; set; }
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs index e4d767d..27d4775 100644 --- a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs @@ -97,6 +97,15 @@ namespace DotNetOAuth.Messaging { }
/// <summary>
+ /// Looks up a localized string similar to The message expired at {0} and it is now {1}..
+ /// </summary>
+ internal static string ExpiredMessage {
+ get {
+ return ResourceManager.GetString("ExpiredMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to This method requires a current HttpContext. Alternatively, use an overload of this method that allows you to pass in information without an HttpContext..
/// </summary>
internal static string HttpContextRequired {
@@ -124,6 +133,24 @@ namespace DotNetOAuth.Messaging { }
/// <summary>
+ /// Looks up a localized string similar to This message has already been processed. This could indicate a replay attack in progress..
+ /// </summary>
+ internal static string ReplayAttackDetected {
+ get {
+ return ResourceManager.GetString("ReplayAttackDetected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This channel does not support replay protection..
+ /// </summary>
+ internal static string ReplayProtectionNotSupported {
+ get {
+ return ResourceManager.GetString("ReplayProtectionNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Message signature was incorrect..
/// </summary>
internal static string SignatureInvalid {
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.resx b/src/DotNetOAuth/Messaging/MessagingStrings.resx index 8548afb..b3024d4 100644 --- a/src/DotNetOAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOAuth/Messaging/MessagingStrings.resx @@ -129,6 +129,9 @@ <data name="ExceptionNotConstructedForTransit" xml:space="preserve">
<value>This exception was not constructed with a root request message that caused it.</value>
</data>
+ <data name="ExpiredMessage" xml:space="preserve">
+ <value>The message expired at {0} and it is now {1}.</value>
+ </data>
<data name="HttpContextRequired" xml:space="preserve">
<value>This method requires a current HttpContext. Alternatively, use an overload of this method that allows you to pass in information without an HttpContext.</value>
</data>
@@ -138,6 +141,12 @@ <data name="QueuedMessageResponseAlreadyExists" xml:space="preserve">
<value>A message response is already queued for sending in the response stream.</value>
</data>
+ <data name="ReplayAttackDetected" xml:space="preserve">
+ <value>This message has already been processed. This could indicate a replay attack in progress.</value>
+ </data>
+ <data name="ReplayProtectionNotSupported" xml:space="preserve">
+ <value>This channel does not support replay protection.</value>
+ </data>
<data name="SignatureInvalid" xml:space="preserve">
<value>Message signature was incorrect.</value>
</data>
|