1
//-----------------------------------------------------------------------
2 // <copyright file="ReturnToSignatureBindingElement.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.OpenId
.ChannelElements
{
9 using System
.Collections
.Generic
;
10 using System
.Collections
.Specialized
;
11 using System
.Security
.Cryptography
;
13 using DotNetOpenAuth
.Messaging
;
14 using DotNetOpenAuth
.OpenId
.Messages
;
17 /// This binding element signs a Relying Party's openid.return_to parameter
18 /// so that upon return, it can verify that it hasn't been tampered with.
21 /// <para>Since Providers can send unsolicited assertions, not all openid.return_to
22 /// values will be signed. But those that are signed will be validated, and
23 /// any invalid or missing signatures will cause this library to not trust
24 /// the parameters in the return_to URL.</para>
25 /// <para>In the messaging stack, this binding element looks like an ordinary
26 /// transform-type of binding element rather than a protection element,
27 /// due to its required order in the channel stack and that it doesn't sign
28 /// anything except a particular message part.</para>
30 internal class ReturnToSignatureBindingElement
: IChannelBindingElement
, IDisposable
{
32 /// The optimal length for a private secret used for signing using the HMACSHA256 class.
35 /// The 64-byte length is optimized for highest security when used with HMACSHA256.
36 /// See HMACSHA256.HMACSHA256(byte[]) documentation for more information.
38 internal static readonly int OptimalPrivateSecretLength
= 64;
41 /// The name of the callback parameter we'll tack onto the return_to value
42 /// to store our signature on the return_to parameter.
44 private const string ReturnToSignatureParameterName
= "dnoi.return_to_sig";
47 /// The hashing algorithm used to generate the private signature on the return_to parameter.
49 private HashAlgorithm signingHasher
;
52 /// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class.
54 /// <param name="secretStore">The secret store from which to retrieve the secret used for signing.</param>
55 internal ReturnToSignatureBindingElement(IPrivateSecretStore secretStore
) {
56 ErrorUtilities
.VerifyArgumentNotNull(secretStore
, "secretStore");
57 ErrorUtilities
.VerifyInternal(secretStore
.PrivateSecret
!= null, "Private secret should have been set already.");
59 if (secretStore
.PrivateSecret
.Length
< OptimalPrivateSecretLength
) {
60 Logger
.WarnFormat("For best security, the optimal length of a private signing secret is {0} bytes, but the secret we have is only {1} bytes.", OptimalPrivateSecretLength
, secretStore
.PrivateSecret
.Length
);
63 this.signingHasher
= new HMACSHA256(secretStore
.PrivateSecret
);
66 #region IChannelBindingElement Members
69 /// Gets or sets the channel that this binding element belongs to.
73 /// This property is set by the channel when it is first constructed.
75 public Channel Channel { get; set; }
78 /// Gets the protection offered (if any) by this binding element.
80 /// <value><see cref="MessageProtections.None"/></value>
81 public MessageProtections Protection
{
82 get { return MessageProtections.None; }
86 /// Prepares a message for sending based on the rules of this channel binding element.
88 /// <param name="message">The message to prepare for sending.</param>
90 /// True if the <paramref name="message"/> applied to this binding element
91 /// and the operation was successful. False otherwise.
94 /// Implementations that provide message protection must honor the
95 /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
97 public bool PrepareMessageForSending(IProtocolMessage message
) {
98 SignedResponseRequest request
= message
as SignedResponseRequest
;
99 if (request
!= null) {
100 string signature
= this.GetReturnToSignature(request
.ReturnTo
);
101 request
.AddReturnToArguments(ReturnToSignatureParameterName
, signature
);
109 /// Performs any transformation on an incoming message that may be necessary and/or
110 /// validates an incoming message based on the rules of this channel binding element.
112 /// <param name="message">The incoming message to process.</param>
114 /// True if the <paramref name="message"/> applied to this binding element
115 /// and the operation was successful. False if the operation did not apply to this message.
117 /// <exception cref="ProtocolException">
118 /// Thrown when the binding element rules indicate that this message is invalid and should
119 /// NOT be processed.
122 /// Implementations that provide message protection must honor the
123 /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
125 public bool PrepareMessageForReceiving(IProtocolMessage message
) {
126 IndirectSignedResponse response
= message
as IndirectSignedResponse
;
128 if (response
!= null) {
129 // We can't use response.GetReturnToArgument(string) because that relies
130 // on us already having validated this signature.
131 NameValueCollection returnToParameters
= HttpUtility
.ParseQueryString(response
.ReturnTo
.Query
);
133 // Set the safety flag showing whether the return_to url had a valid signature.
134 string expected
= this.GetReturnToSignature(response
.ReturnTo
);
135 string actual
= returnToParameters
[ReturnToSignatureParameterName
];
136 actual
= OpenIdUtilities
.FixDoublyUriDecodedBase64String(actual
);
137 response
.ReturnToParametersSignatureValidated
= actual
== expected
;
138 if (!response
.ReturnToParametersSignatureValidated
) {
139 Logger
.WarnFormat("The return_to signature failed verification.");
150 #region IDisposable Members
153 /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
155 public void Dispose() {
157 GC
.SuppressFinalize(this);
161 /// Releases unmanaged and - optionally - managed resources
163 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
164 protected virtual void Dispose(bool disposing
) {
166 IDisposable hasher
= this.signingHasher
as IDisposable
;
167 if (hasher
!= null) {
176 /// Gets the return to signature.
178 /// <param name="returnTo">The return to.</param>
179 /// <returns>The generated signature.</returns>
181 /// Only the parameters in the return_to URI are signed, rather than the base URI
182 /// itself, in order that OPs that might change the return_to's implicit port :80 part
183 /// or other minor changes do not invalidate the signature.
185 private string GetReturnToSignature(Uri returnTo
) {
186 ErrorUtilities
.VerifyArgumentNotNull(returnTo
, "returnTo");
188 // Assemble the dictionary to sign, taking care to remove the signature itself
189 // in order to accurately reproduce the original signature (which of course didn't include
191 // Also we need to sort the dictionary's keys so that we sign in the same order as we did
193 var returnToParameters
= HttpUtility
.ParseQueryString(returnTo
.Query
).ToDictionary();
194 returnToParameters
.Remove(ReturnToSignatureParameterName
);
195 var sortedReturnToParameters
= new SortedDictionary
<string, string>(StringComparer
.OrdinalIgnoreCase
);
196 foreach (var pair
in returnToParameters
) {
197 sortedReturnToParameters
.Add(pair
.Key
, pair
.Value
);
200 Logger
.DebugFormat("ReturnTo signed data: {0}{1}", Environment
.NewLine
, sortedReturnToParameters
.ToStringDeferred());
202 // Sign the parameters.
203 byte[] bytesToSign
= KeyValueFormEncoding
.GetBytes(sortedReturnToParameters
);
204 byte[] signature
= this.signingHasher
.ComputeHash(bytesToSign
);
205 string signatureBase64
= Convert
.ToBase64String(signature
);
206 return signatureBase64
;