diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/ChannelElements')
11 files changed, 1476 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs new file mode 100644 index 0000000..b730b1f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs @@ -0,0 +1,129 @@ +//----------------------------------------------------------------------- +// <copyright file="BackwardCompatibilityBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Provides a mechanism for Relying Parties to work with OpenID 1.0 Providers + /// without losing claimed_id and op_endpoint data, which OpenID 2.0 Providers + /// are required to send back with positive assertions. + /// </summary> + internal class BackwardCompatibilityBindingElement : IChannelBindingElement { + /// <summary> + /// The "dnoa.op_endpoint" callback parameter that stores the Provider Endpoint URL + /// to tack onto the return_to URI. + /// </summary> + private const string ProviderEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + + /// <summary> + /// The "dnoa.claimed_id" callback parameter that stores the Claimed Identifier + /// to tack onto the return_to URI. + /// </summary> + private const string ClaimedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + SignedResponseRequest request = message as SignedResponseRequest; + if (request != null && request.Version.Major < 2) { + request.AddReturnToArguments(ProviderEndpointParameterName, request.Recipient.AbsoluteUri); + + CheckIdRequest authRequest = request as CheckIdRequest; + if (authRequest != null) { + request.AddReturnToArguments(ClaimedIdentifierParameterName, authRequest.ClaimedIdentifier); + } + + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + IndirectSignedResponse response = message as IndirectSignedResponse; + if (response != null && response.Version.Major < 2) { + // GetReturnToArgument may return parameters that are not signed, + // but we must allow for that since in OpenID 1.x, a stateless RP has + // no way to preserve the provider endpoint and claimed identifier otherwise. + // We'll verify the positive assertion later in the + // RelyingParty.PositiveAuthenticationResponse constructor anyway. + // If this is a 1.0 OP signed response without these parameters then we didn't initiate + // the request ,and since 1.0 OPs are not supposed to be able to send unsolicited + // assertions it's an invalid case that we throw an exception for. + if (response.ProviderEndpoint == null) { + string op_endpoint = response.GetReturnToArgument(ProviderEndpointParameterName); + ErrorUtilities.VerifyProtocol(op_endpoint != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ProviderEndpointParameterName); + response.ProviderEndpoint = new Uri(op_endpoint); + } + + PositiveAssertionResponse authResponse = response as PositiveAssertionResponse; + if (authResponse != null) { + if (authResponse.ClaimedIdentifier == null) { + string claimedId = response.GetReturnToArgument(ClaimedIdentifierParameterName); + ErrorUtilities.VerifyProtocol(claimedId != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ClaimedIdentifierParameterName); + authResponse.ClaimedIdentifier = claimedId; + } + } + + return MessageProtections.None; + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs new file mode 100644 index 0000000..705f737 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs @@ -0,0 +1,251 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionsBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The binding element that serializes/deserializes OpenID extensions to/from + /// their carrying OpenID messages. + /// </summary> + internal class ExtensionsBindingElement : IChannelBindingElement { + /// <summary> + /// False if unsigned extensions should be dropped. Must always be true on Providers, since RPs never sign extensions. + /// </summary> + private readonly bool receiveUnsignedExtensions; + + /// <summary> + /// Initializes a new instance of the <see cref="ExtensionsBindingElement"/> class. + /// </summary> + /// <param name="extensionFactory">The extension factory.</param> + /// <param name="securitySettings">The security settings.</param> + /// <param name="receiveUnsignedExtensions">Security setting for relying parties. Should be true for Providers.</param> + internal ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory, SecuritySettings securitySettings, bool receiveUnsignedExtensions) { + Requires.NotNull(extensionFactory, "extensionFactory"); + Requires.NotNull(securitySettings, "securitySettings"); + + this.ExtensionFactory = extensionFactory; + this.receiveUnsignedExtensions = receiveUnsignedExtensions; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the extension factory. + /// </summary> + public IOpenIdExtensionFactory ExtensionFactory { get; private set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "It doesn't look too bad to me. :)")] + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var extendableMessage = message as IProtocolMessageWithExtensions; + if (extendableMessage != null) { + Protocol protocol = Protocol.Lookup(message.Version); + MessageDictionary baseMessageDictionary = this.Channel.MessageDescriptions.GetAccessor(message); + + // We have a helper class that will do all the heavy-lifting of organizing + // all the extensions, their aliases, and their parameters. + var extensionManager = ExtensionArgumentsManager.CreateOutgoingExtensions(protocol); + foreach (IExtensionMessage protocolExtension in extendableMessage.Extensions) { + var extension = protocolExtension as IOpenIdMessageExtension; + if (extension != null) { + Reporting.RecordFeatureUse(protocolExtension); + + // Give extensions that require custom serialization a chance to do their work. + var customSerializingExtension = extension as IMessageWithEvents; + if (customSerializingExtension != null) { + customSerializingExtension.OnSending(); + } + + // OpenID 2.0 Section 12 forbids two extensions with the same TypeURI in the same message. + ErrorUtilities.VerifyProtocol(!extensionManager.ContainsExtension(extension.TypeUri), OpenIdStrings.ExtensionAlreadyAddedWithSameTypeURI, extension.TypeUri); + + // Ensure that we're sending out a valid extension. + var extensionDescription = this.Channel.MessageDescriptions.Get(extension); + var extensionDictionary = extensionDescription.GetDictionary(extension).Serialize(); + extensionDescription.EnsureMessagePartsPassBasicValidation(extensionDictionary); + + // Add the extension to the outgoing message payload. + extensionManager.AddExtensionArguments(extension.TypeUri, extensionDictionary); + } else { + Logger.OpenId.WarnFormat("Unexpected extension type {0} did not implement {1}.", protocolExtension.GetType(), typeof(IOpenIdMessageExtension).Name); + } + } + + // We use a cheap trick (for now at least) to determine whether the 'openid.' prefix + // belongs on the parameters by just looking at what other parameters do. + // Technically, direct message responses from Provider to Relying Party are the only + // messages that leave off the 'openid.' prefix. + bool includeOpenIdPrefix = baseMessageDictionary.Keys.Any(key => key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)); + + // Add the extension parameters to the base message for transmission. + baseMessageDictionary.AddExtraParameters(extensionManager.GetArgumentsToSend(includeOpenIdPrefix)); + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var extendableMessage = message as IProtocolMessageWithExtensions; + if (extendableMessage != null) { + // First add the extensions that are signed by the Provider. + foreach (IOpenIdMessageExtension signedExtension in this.GetExtensions(extendableMessage, true, null)) { + Reporting.RecordFeatureUse(signedExtension); + signedExtension.IsSignedByRemoteParty = true; + extendableMessage.Extensions.Add(signedExtension); + } + + // Now search again, considering ALL extensions whether they are signed or not, + // skipping the signed ones and adding the new ones as unsigned extensions. + if (this.receiveUnsignedExtensions) { + Func<string, bool> isNotSigned = typeUri => !extendableMessage.Extensions.Cast<IOpenIdMessageExtension>().Any(ext => ext.TypeUri == typeUri); + foreach (IOpenIdMessageExtension unsignedExtension in this.GetExtensions(extendableMessage, false, isNotSigned)) { + Reporting.RecordFeatureUse(unsignedExtension); + unsignedExtension.IsSignedByRemoteParty = false; + extendableMessage.Extensions.Add(unsignedExtension); + } + } + + return MessageProtections.None; + } + + return null; + } + + #endregion + + /// <summary> + /// Gets the extensions on a message. + /// </summary> + /// <param name="message">The carrier of the extensions.</param> + /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param> + /// <param name="extensionFilter">A optional filter that takes an extension type URI and + /// returns a value indicating whether that extension should be deserialized and + /// returned in the sequence. May be null.</param> + /// <returns>A sequence of extensions in the message.</returns> + private IEnumerable<IOpenIdMessageExtension> GetExtensions(IProtocolMessageWithExtensions message, bool ignoreUnsigned, Func<string, bool> extensionFilter) { + bool isAtProvider = message is SignedResponseRequest; + + // We have a helper class that will do all the heavy-lifting of organizing + // all the extensions, their aliases, and their parameters. + var extensionManager = ExtensionArgumentsManager.CreateIncomingExtensions(this.GetExtensionsDictionary(message, ignoreUnsigned)); + foreach (string typeUri in extensionManager.GetExtensionTypeUris()) { + // Our caller may have already obtained a signed version of this extension, + // so skip it if they don't want this one. + if (extensionFilter != null && !extensionFilter(typeUri)) { + continue; + } + + var extensionData = extensionManager.GetExtensionArguments(typeUri); + + // Initialize this particular extension. + IOpenIdMessageExtension extension = this.ExtensionFactory.Create(typeUri, extensionData, message, isAtProvider); + if (extension != null) { + try { + // Make sure the extension fulfills spec requirements before deserializing it. + MessageDescription messageDescription = this.Channel.MessageDescriptions.Get(extension); + messageDescription.EnsureMessagePartsPassBasicValidation(extensionData); + + // Deserialize the extension. + MessageDictionary extensionDictionary = messageDescription.GetDictionary(extension); + foreach (var pair in extensionData) { + extensionDictionary[pair.Key] = pair.Value; + } + + // Give extensions that require custom serialization a chance to do their work. + var customSerializingExtension = extension as IMessageWithEvents; + if (customSerializingExtension != null) { + customSerializingExtension.OnReceiving(); + } + } catch (ProtocolException ex) { + Logger.OpenId.ErrorFormat(OpenIdStrings.BadExtension, extension.GetType(), ex); + extension = null; + } + + if (extension != null) { + yield return extension; + } + } else { + Logger.OpenId.DebugFormat("Extension with type URI '{0}' ignored because it is not a recognized extension.", typeUri); + } + } + } + + /// <summary> + /// Gets the dictionary of message parts that should be deserialized into extensions. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param> + /// <returns> + /// A dictionary of message parts, including only signed parts when appropriate. + /// </returns> + private IDictionary<string, string> GetExtensionsDictionary(IProtocolMessage message, bool ignoreUnsigned) { + Requires.ValidState(this.Channel != null); + + IndirectSignedResponse signedResponse = message as IndirectSignedResponse; + if (signedResponse != null && ignoreUnsigned) { + return signedResponse.GetSignedMessageParts(this.Channel); + } else { + return this.Channel.MessageDescriptions.GetAccessor(message); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs new file mode 100644 index 0000000..0c8d95e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdExtensionFactory.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// OpenID extension factory class for creating extensions based on received Type URIs. + /// </summary> + /// <remarks> + /// OpenID extension factories must be registered with the library. This can be + /// done by adding a factory to OpenIdRelyingParty.ExtensionFactories + /// or OpenIdProvider.ExtensionFactories, or by adding a snippet + /// such as the following to your web.config file: + /// <example> + /// <dotNetOpenAuth> + /// <openid> + /// <extensionFactories> + /// <add type="DotNetOpenAuth.ApplicationBlock.CustomExtensions.Acme, DotNetOpenAuth.ApplicationBlock" /> + /// </extensionFactories> + /// </openid> + /// </dotNetOpenAuth> + /// </example> + /// </remarks> + public interface IOpenIdExtensionFactory { + /// <summary> + /// Creates a new instance of some extension based on the received extension parameters. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + /// <remarks> + /// This factory method need only initialize properties in the instantiated extension object + /// that are not bound using <see cref="MessagePartAttribute"/>. + /// </remarks> + IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs new file mode 100644 index 0000000..533e818 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// <copyright file="ITamperResistantOpenIdMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An interface that OAuth messages implement to support signing. + /// </summary> + internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage, IReplayProtectedProtocolMessage { + /// <summary> + /// Gets or sets the association handle used to sign the message. + /// </summary> + /// <value>The handle for the association that was used to sign this assertion. </value> + string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. + /// </summary> + /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value> + string InvalidateHandle { get; set; } + + /// <summary> + /// Gets or sets the signed parameter order. + /// </summary> + /// <value>Comma-separated list of signed fields.</value> + /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example> + /// <remarks> + /// This entry consists of the fields without the "openid." prefix that the signature covers. + /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle", + /// and if present in the response, "claimed_id" and "identity". + /// Additional keys MAY be signed as part of the message. See Generating Signatures. + /// </remarks> + string SignedParameterOrder { get; set; } // TODO: make sure we have a unit test to verify that an incoming message with fewer signed fields than required will be rejected. + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs new file mode 100644 index 0000000..1993cb4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------- +// <copyright file="KeyValueFormEncoding.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Indicates the level of strictness to require when decoding a + /// Key-Value Form encoded dictionary. + /// </summary> + public enum KeyValueFormConformanceLevel { + /// <summary> + /// Be as forgiving as possible to errors made while encoding. + /// </summary> + Loose, + + /// <summary> + /// Allow for certain errors in encoding attributable to ambiguities + /// in the OpenID 1.1 spec's description of the encoding. + /// </summary> + OpenId11, + + /// <summary> + /// The strictest mode. The decoder requires the encoded dictionary + /// to be in strict compliance with OpenID 2.0's description of + /// the encoding. + /// </summary> + OpenId20, + } + + /// <summary> + /// Performs conversion to and from the Key-Value Form Encoding defined by + /// OpenID Authentication 2.0 section 4.1.1. + /// http://openid.net/specs/openid-authentication-2_0.html#anchor4 + /// </summary> + /// <remarks> + /// This class is thread safe and immutable. + /// </remarks> + internal class KeyValueFormEncoding { + /// <summary> + /// Characters that must not appear in parameter names. + /// </summary> + private static readonly char[] IllegalKeyCharacters = { '\n', ':' }; + + /// <summary> + /// Characters that must not appaer in parameter values. + /// </summary> + private static readonly char[] IllegalValueCharacters = { '\n' }; + + /// <summary> + /// The newline character sequence to use. + /// </summary> + private const string NewLineCharacters = "\n"; + + /// <summary> + /// The character encoding to use. + /// </summary> + private static readonly Encoding textEncoding = new UTF8Encoding(false); + + /// <summary> + /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class. + /// </summary> + public KeyValueFormEncoding() { + this.ConformanceLevel = KeyValueFormConformanceLevel.Loose; + } + + /// <summary> + /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class. + /// </summary> + /// <param name="conformanceLevel">How strictly an incoming Key-Value Form message will be held to the spec.</param> + public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) { + this.ConformanceLevel = conformanceLevel; + } + + /// <summary> + /// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec. + /// </summary> + public KeyValueFormConformanceLevel ConformanceLevel { get; private set; } + + /// <summary> + /// Encodes key/value pairs to Key-Value Form. + /// </summary> + /// <param name="keysAndValues"> + /// The dictionary of key/value pairs to convert to a byte stream. + /// </param> + /// <returns>The UTF8 byte array.</returns> + /// <remarks> + /// Enumerating a Dictionary<TKey, TValue> has undeterministic ordering. + /// If ordering of the key=value pairs is important, a deterministic enumerator must + /// be used. + /// </remarks> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")] + public static byte[] GetBytes(IEnumerable<KeyValuePair<string, string>> keysAndValues) { + Requires.NotNull(keysAndValues, "keysAndValues"); + + using (MemoryStream ms = new MemoryStream()) { + using (StreamWriter sw = new StreamWriter(ms, textEncoding)) { + sw.NewLine = NewLineCharacters; + foreach (var pair in keysAndValues) { + if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key)); + } + if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value)); + } + + sw.Write(pair.Key); + sw.Write(':'); + sw.Write(pair.Value); + sw.WriteLine(); + } + } + + return ms.ToArray(); + } + } + + /// <summary> + /// Decodes bytes in Key-Value Form to key/value pairs. + /// </summary> + /// <param name="data">The stream of Key-Value Form encoded bytes.</param> + /// <returns>The deserialized dictionary.</returns> + /// <exception cref="FormatException">Thrown when the data is not in the expected format.</exception> + public IDictionary<string, string> GetDictionary(Stream data) { + using (StreamReader reader = new StreamReader(data, textEncoding)) { + var dict = new Dictionary<string, string>(); + int line_num = 0; + string line; + while ((line = reader.ReadLine()) != null) { + line_num++; + if (this.ConformanceLevel == KeyValueFormConformanceLevel.Loose) { + line = line.Trim(); + if (line.Length == 0) { + continue; + } + } + string[] parts = line.Split(new[] { ':' }, 2); + ErrorUtilities.VerifyFormat(parts.Length == 2, OpenIdStrings.InvalidKeyValueFormCharacterMissing, ':', line_num, line); + if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) { + ErrorUtilities.VerifyFormat(!(char.IsWhiteSpace(parts[0], parts[0].Length - 1) || char.IsWhiteSpace(parts[1], 0)), OpenIdStrings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line); + } + if (this.ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) { + parts[0] = parts[0].Trim(); + parts[1] = parts[1].Trim(); + } + + // calling Add method will throw if a key is encountered twice, + // which we should do. + dict.Add(parts[0], parts[1]); + } + if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) { + reader.BaseStream.Seek(-1, SeekOrigin.End); + ErrorUtilities.VerifyFormat(reader.BaseStream.ReadByte() == '\n', OpenIdStrings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line); + } + return dict; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs new file mode 100644 index 0000000..a2a5c88 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs @@ -0,0 +1,228 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A channel that knows how to send and receive OpenID messages. + /// </summary> + [ContractVerification(true)] + internal class OpenIdChannel : Channel { + /// <summary> + /// The HTTP Content-Type to use in Key-Value Form responses. + /// </summary> + /// <remarks> + /// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain. But this value + /// does not prevent free hosters like GoDaddy from tacking on their ads + /// to the end of the direct response, corrupting the data. So we deviate + /// from the spec a bit here to improve the story for free Providers. + /// </remarks> + internal const string KeyValueFormContentType = "application/x-openid-kvf"; + + /// <summary> + /// The encoder that understands how to read and write Key-Value Form. + /// </summary> + private KeyValueFormEncoding keyValueForm = new KeyValueFormEncoding(); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdChannel"/> class. + /// </summary> + /// <param name="messageTypeProvider">A class prepared to analyze incoming messages and indicate what concrete + /// message types can deserialize from it.</param> + /// <param name="bindingElements">The binding elements to use in sending and receiving messages.</param> + protected OpenIdChannel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements) + : base(messageTypeProvider, bindingElements) { + Requires.NotNull(messageTypeProvider, "messageTypeProvider"); + + // Customize the binding element order, since we play some tricks for higher + // security and backward compatibility with older OpenID versions. + var outgoingBindingElements = new List<IChannelBindingElement>(bindingElements); + var incomingBindingElements = new List<IChannelBindingElement>(bindingElements); + incomingBindingElements.Reverse(); + + // Customize the order of the incoming elements by moving the return_to elements in front. + var backwardCompatibility = incomingBindingElements.OfType<BackwardCompatibilityBindingElement>().SingleOrDefault(); + var returnToSign = incomingBindingElements.OfType<ReturnToSignatureBindingElement>().SingleOrDefault(); + if (backwardCompatibility != null) { + incomingBindingElements.MoveTo(0, backwardCompatibility); + } + if (returnToSign != null) { + // Yes, this is intentionally, shifting the backward compatibility + // binding element to second position. + incomingBindingElements.MoveTo(0, returnToSign); + } + + this.CustomizeBindingElementOrder(outgoingBindingElements, incomingBindingElements); + + // Change out the standard web request handler to reflect the standard + // OpenID pattern that outgoing web requests are to unknown and untrusted + // servers on the Internet. + this.WebRequestHandler = new UntrustedWebRequestHandler(); + } + + /// <summary> + /// Verifies the integrity and applicability of an incoming message. + /// </summary> + /// <param name="message">The message just received.</param> + /// <exception cref="ProtocolException"> + /// Thrown when the message is somehow invalid, except for check_authentication messages. + /// This can be due to tampering, replay attack or expiration, among other things. + /// </exception> + protected override void ProcessIncomingMessage(IProtocolMessage message) { + var checkAuthRequest = message as CheckAuthenticationRequest; + if (checkAuthRequest != null) { + IndirectSignedResponse originalResponse = new IndirectSignedResponse(checkAuthRequest, this); + try { + base.ProcessIncomingMessage(originalResponse); + checkAuthRequest.IsValid = true; + } catch (ProtocolException) { + checkAuthRequest.IsValid = false; + } + } else { + base.ProcessIncomingMessage(message); + } + + // Convert an OpenID indirect error message, which we never expect + // between two good OpenID implementations, into an exception. + // We don't process DirectErrorResponse because associate negotiations + // commonly get a derivative of that message type and handle it. + var errorMessage = message as IndirectErrorResponse; + if (errorMessage != null) { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + OpenIdStrings.IndirectErrorFormattedMessage, + errorMessage.ErrorMessage, + errorMessage.Contact, + errorMessage.Reference); + throw new ProtocolException(exceptionMessage, message); + } + } + + /// <summary> + /// Prepares an HTTP request that carries a given message. + /// </summary> + /// <param name="request">The message to send.</param> + /// <returns> + /// The <see cref="HttpWebRequest"/> prepared to send the request. + /// </returns> + protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + return this.InitializeRequestAsPost(request); + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + try { + return this.keyValueForm.GetDictionary(response.ResponseStream); + } catch (FormatException ex) { + throw ErrorUtilities.Wrap(ex, ex.Message); + } + } + + /// <summary> + /// Called when receiving a direct response message, before deserialization begins. + /// </summary> + /// <param name="response">The HTTP direct response.</param> + /// <param name="message">The newly instantiated message, prior to deserialization.</param> + protected override void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) { + base.OnReceivingDirectResponse(response, message); + + // Verify that the expected HTTP status code was used for the message, + // per OpenID 2.0 section 5.1.2.2. + // Note: The v1.1 spec doesn't require 400 responses for some error messages + if (message.Version.Major >= 2) { + var httpDirectResponse = message as IHttpDirectResponse; + if (httpDirectResponse != null) { + ErrorUtilities.VerifyProtocol( + httpDirectResponse.HttpStatusCode == response.Status, + MessagingStrings.UnexpectedHttpStatusCode, + (int)httpDirectResponse.HttpStatusCode, + (int)response.Status); + } + } + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns> + /// The pending user agent redirect based message to be sent as an HttpResponse. + /// </returns> + /// <remarks> + /// This method implements spec V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var messageAccessor = this.MessageDescriptions.GetAccessor(response); + var fields = messageAccessor.Serialize(); + byte[] keyValueEncoding = KeyValueFormEncoding.GetBytes(fields); + + OutgoingWebResponse preparedResponse = new OutgoingWebResponse(); + preparedResponse.Headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType); + preparedResponse.OriginalMessage = response; + preparedResponse.ResponseStream = new MemoryStream(keyValueEncoding); + + IHttpDirectResponse httpMessage = response as IHttpDirectResponse; + if (httpMessage != null) { + preparedResponse.Status = httpMessage.HttpStatusCode; + } + + return preparedResponse; + } + + /// <summary> + /// Gets the direct response of a direct HTTP request. + /// </summary> + /// <param name="webRequest">The web request.</param> + /// <returns>The response to the web request.</returns> + /// <exception cref="ProtocolException">Thrown on network or protocol errors.</exception> + protected override IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) { + IncomingWebResponse response = this.WebRequestHandler.GetResponse(webRequest, DirectWebRequestOptions.AcceptAllHttpResponses); + + // Filter the responses to the allowable set of HTTP status codes. + if (response.Status != HttpStatusCode.OK && response.Status != HttpStatusCode.BadRequest) { + if (Logger.Channel.IsErrorEnabled) { + using (var reader = new StreamReader(response.ResponseStream)) { + Logger.Channel.ErrorFormat( + "Unexpected HTTP status code {0} {1} received in direct response:{2}{3}", + (int)response.Status, + response.Status, + Environment.NewLine, + reader.ReadToEnd()); + } + } + + // Call dispose before throwing since we're not including the response in the + // exception we're throwing. + response.Dispose(); + + ErrorUtilities.ThrowProtocol(OpenIdStrings.UnexpectedHttpStatusCode, (int)response.Status, response.Status); + } + + return response; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs new file mode 100644 index 0000000..75b01e1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="OriginalStringUriEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// A Uri encoder that serializes using <see cref="Uri.OriginalString"/> + /// rather than the standard <see cref="Uri.AbsoluteUri"/>. + /// </summary> + internal class OriginalStringUriEncoder : IMessagePartEncoder { + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + Uri uriValue = (Uri)value; + return uriValue != null ? uriValue.OriginalString : null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return value != null ? new Uri(value) : null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs new file mode 100644 index 0000000..25a29bb --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -0,0 +1,210 @@ +//----------------------------------------------------------------------- +// <copyright file="ReturnToSignatureBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// This binding element signs a Relying Party's openid.return_to parameter + /// so that upon return, it can verify that it hasn't been tampered with. + /// </summary> + /// <remarks> + /// <para>Since Providers can send unsolicited assertions, not all openid.return_to + /// values will be signed. But those that are signed will be validated, and + /// any invalid or missing signatures will cause this library to not trust + /// the parameters in the return_to URL.</para> + /// <para>In the messaging stack, this binding element looks like an ordinary + /// transform-type of binding element rather than a protection element, + /// due to its required order in the channel stack and that it doesn't sign + /// anything except a particular message part.</para> + /// </remarks> + internal class ReturnToSignatureBindingElement : IChannelBindingElement { + /// <summary> + /// The name of the callback parameter we'll tack onto the return_to value + /// to store our signature on the return_to parameter. + /// </summary> + private const string ReturnToSignatureParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig"; + + /// <summary> + /// The name of the callback parameter we'll tack onto the return_to value + /// to store the handle of the association we use to sign the return_to parameter. + /// </summary> + private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle"; + + /// <summary> + /// The URI to use for private associations at this RP. + /// </summary> + private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret"); + + /// <summary> + /// The key store used to generate the private signature on the return_to parameter. + /// </summary> + private ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) { + Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); + + this.cryptoKeyStore = cryptoKeyStore; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + /// <remarks> + /// No message protection is reported because this binding element + /// does not protect the entire message -- only a part. + /// </remarks> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + SignedResponseRequest request = message as SignedResponseRequest; + if (request != null && request.ReturnTo != null && request.SignReturnTo) { + var cryptoKeyPair = this.cryptoKeyStore.GetCurrentKey(SecretUri.AbsoluteUri, OpenIdElement.Configuration.MaxAuthenticationTime); + request.AddReturnToArguments(ReturnToSignatureHandleParameterName, cryptoKeyPair.Key); + string signature = Convert.ToBase64String(this.GetReturnToSignature(request.ReturnTo, cryptoKeyPair.Value)); + request.AddReturnToArguments(ReturnToSignatureParameterName, signature); + + // We return none because we are not signing the entire message (only a part). + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + IndirectSignedResponse response = message as IndirectSignedResponse; + + if (response != null) { + // We can't use response.GetReturnToArgument(string) because that relies + // on us already having validated this signature. + NameValueCollection returnToParameters = HttpUtility.ParseQueryString(response.ReturnTo.Query); + + // Only check the return_to signature if one is present. + if (returnToParameters[ReturnToSignatureHandleParameterName] != null) { + // Set the safety flag showing whether the return_to url had a valid signature. + byte[] expectedBytes = this.GetReturnToSignature(response.ReturnTo); + string actual = returnToParameters[ReturnToSignatureParameterName]; + actual = OpenIdUtilities.FixDoublyUriDecodedBase64String(actual); + byte[] actualBytes = Convert.FromBase64String(actual); + response.ReturnToParametersSignatureValidated = MessagingUtilities.AreEquivalentConstantTime(actualBytes, expectedBytes); + if (!response.ReturnToParametersSignatureValidated) { + Logger.Bindings.WarnFormat("The return_to signature failed verification."); + } + + return MessageProtections.None; + } + } + + return null; + } + + #endregion + + /// <summary> + /// Gets the return to signature. + /// </summary> + /// <param name="returnTo">The return to.</param> + /// <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 + /// or other minor changes do not invalidate the signature. + /// </remarks> + private byte[] GetReturnToSignature(Uri returnTo, CryptoKey cryptoKey = null) { + Requires.NotNull(returnTo, "returnTo"); + + // Assemble the dictionary to sign, taking care to remove the signature itself + // in order to accurately reproduce the original signature (which of course didn't include + // the signature). + // Also we need to sort the dictionary's keys so that we sign in the same order as we did + // the last time. + var returnToParameters = HttpUtility.ParseQueryString(returnTo.Query); + returnToParameters.Remove(ReturnToSignatureParameterName); + var sortedReturnToParameters = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + foreach (string key in returnToParameters) { + sortedReturnToParameters.Add(key, returnToParameters[key]); + } + + Logger.Bindings.DebugFormat("ReturnTo signed data: {0}{1}", Environment.NewLine, sortedReturnToParameters.ToStringDeferred()); + + // Sign the parameters. + byte[] bytesToSign = KeyValueFormEncoding.GetBytes(sortedReturnToParameters); + byte[] signature; + try { + if (cryptoKey == null) { + cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]); + } + + using (var signer = new HMACSHA256(cryptoKey.Key)) { + signature = signer.ComputeHash(bytesToSign); + } + } catch (ProtocolException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired); + } + + return signature; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs new file mode 100644 index 0000000..d14ca8a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Web; + using DotNetOpenAuth.Loggers; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Signs and verifies authentication assertions. + /// </summary> + [ContractClass(typeof(SigningBindingElementContract))] + internal abstract class SigningBindingElement : IChannelBindingElement { + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.TamperProtection"/></value> + public MessageProtections Protection { + get { return MessageProtections.TamperProtection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + /// <summary> + /// Gets a value indicating whether this binding element is on a Provider channel. + /// </summary> + protected virtual bool IsOnProvider { + get { return false; } + } + + #region IChannelBindingElement Methods + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public virtual MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature); + MessageProtections protectionsApplied = MessageProtections.TamperProtection; + + this.EnsureParametersRequiringSignatureAreSigned(signedMessage); + + Association association = this.GetSpecificAssociation(signedMessage); + if (association != null) { + string signature = this.GetSignature(signedMessage, association); + if (!MessagingUtilities.EqualsConstantTime(signedMessage.Signature, signature)) { + Logger.Bindings.Error("Signature verification failed."); + throw new InvalidSignatureException(message); + } + } else { + ErrorUtilities.VerifyInternal(this.Channel != null, "Cannot verify private association signature because we don't have a channel."); + + protectionsApplied = this.VerifySignatureByUnrecognizedHandle(message, signedMessage, protectionsApplied); + } + + return protectionsApplied; + } + + return null; + } + + /// <summary> + /// Verifies the signature by unrecognized handle. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="signedMessage">The signed message.</param> + /// <param name="protectionsApplied">The protections applied.</param> + /// <returns>The applied protections.</returns> + protected abstract MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied); + + #endregion + + /// <summary> + /// Calculates the signature for a given message. + /// </summary> + /// <param name="signedMessage">The message to sign or verify.</param> + /// <param name="association">The association to use to sign the message.</param> + /// <returns>The calculated signature of the method.</returns> + protected string GetSignature(ITamperResistantOpenIdMessage signedMessage, Association association) { + Requires.NotNull(signedMessage, "signedMessage"); + Requires.True(!String.IsNullOrEmpty(signedMessage.SignedParameterOrder), "signedMessage"); + Requires.NotNull(association, "association"); + + // Prepare the parts to sign, taking care to replace an openid.mode value + // of check_authentication with its original id_res so the signature matches. + MessageDictionary dictionary = this.Channel.MessageDescriptions.GetAccessor(signedMessage); + var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',') + let prefixedName = Protocol.V20.openid.Prefix + name + select new KeyValuePair<string, string>(name, dictionary.GetValueOrThrow(prefixedName, signedMessage)); + + byte[] dataToSign = KeyValueFormEncoding.GetBytes(parametersToSign); + string signature = Convert.ToBase64String(association.Sign(dataToSign)); + + if (Logger.Signatures.IsDebugEnabled) { + Logger.Signatures.DebugFormat( + "Signing these message parts: {0}{1}{0}Base64 representation of signed data: {2}{0}Signature: {3}", + Environment.NewLine, + parametersToSign.ToStringDeferred(), + Convert.ToBase64String(dataToSign), + signature); + } + + return signature; + } + + /// <summary> + /// Gets the association to use to sign or verify a message. + /// </summary> + /// <param name="signedMessage">The message to sign or verify.</param> + /// <returns>The association to use to sign or verify the message.</returns> + protected abstract Association GetAssociation(ITamperResistantOpenIdMessage signedMessage); + + /// <summary> + /// Gets a specific association referenced in a given message's association handle. + /// </summary> + /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param> + /// <returns>The referenced association; or <c>null</c> if such an association cannot be found.</returns> + /// <remarks> + /// If the association handle set in the message does not match any valid association, + /// the association handle property is cleared, and the + /// <see cref="ITamperResistantOpenIdMessage.InvalidateHandle"/> property is set to the + /// handle that could not be found. + /// </remarks> + protected abstract Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage); + + /// <summary> + /// Gets a private Provider association used for signing messages in "dumb" mode. + /// </summary> + /// <returns>An existing or newly created association.</returns> + protected virtual Association GetDumbAssociationForSigning() { + throw new NotImplementedException(); + } + + /// <summary> + /// Ensures that all message parameters that must be signed are in fact included + /// in the signature. + /// </summary> + /// <param name="signedMessage">The signed message.</param> + private void EnsureParametersRequiringSignatureAreSigned(ITamperResistantOpenIdMessage signedMessage) { + // Verify that the signed parameter order includes the mandated fields. + // We do this in such a way that derived classes that add mandated fields automatically + // get included in the list of checked parameters. + Protocol protocol = Protocol.Lookup(signedMessage.Version); + var partsRequiringProtection = from part in this.Channel.MessageDescriptions.Get(signedMessage).Mapping.Values + where part.RequiredProtection != ProtectionLevel.None + where part.IsRequired || part.IsNondefaultValueSet(signedMessage) + select part.Name; + ErrorUtilities.VerifyInternal(partsRequiringProtection.All(name => name.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)), "Signing only works when the parameters start with the 'openid.' prefix."); + string[] signedParts = signedMessage.SignedParameterOrder.Split(','); + var unsignedParts = from partName in partsRequiringProtection + where !signedParts.Contains(partName.Substring(protocol.openid.Prefix.Length)) + select partName; + ErrorUtilities.VerifyProtocol(!unsignedParts.Any(), OpenIdStrings.SignatureDoesNotIncludeMandatoryParts, string.Join(", ", unsignedParts.ToArray())); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs new file mode 100644 index 0000000..bf8b18d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementContract.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Web; + using DotNetOpenAuth.Loggers; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Code contract for the <see cref="SigningBindingElement"/> class. + /// </summary> + [ContractClassFor(typeof(SigningBindingElement))] + internal abstract class SigningBindingElementContract : SigningBindingElement { + /// <summary> + /// Verifies the signature by unrecognized handle. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="signedMessage">The signed message.</param> + /// <param name="protectionsApplied">The protections applied.</param> + /// <returns> + /// The applied protections. + /// </returns> + protected override MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied) { + throw new NotImplementedException(); + } + + /// <summary> + /// Gets the association to use to sign or verify a message. + /// </summary> + /// <param name="signedMessage">The message to sign or verify.</param> + /// <returns> + /// The association to use to sign or verify the message. + /// </returns> + protected override Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) { + Requires.NotNull(signedMessage, "signedMessage"); + throw new NotImplementedException(); + } + + /// <summary> + /// Gets a specific association referenced in a given message's association handle. + /// </summary> + /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param> + /// <returns> + /// The referenced association; or <c>null</c> if such an association cannot be found. + /// </returns> + protected override Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) { + Requires.NotNull(signedMessage, "signedMessage"); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs new file mode 100644 index 0000000..ad65a83 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------- +// <copyright file="SkipSecurityBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Spoofs security checks on incoming OpenID messages. + /// </summary> + internal class SkipSecurityBindingElement : IChannelBindingElement { + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.All"/></value> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public MessageProtections Protection { + get { return MessageProtections.All; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + Debug.Fail("SkipSecurityBindingElement.ProcessOutgoingMessage should never be called."); + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.Bindings.DebugFormat("Skipped security checks of incoming {0} message for preview purposes.", message.GetType().Name); + return this.Protection; + } + + return null; + } + + #endregion + } +} |