FxCop fixes.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / ChannelElements / SigningBindingElement.cs
blobd912442bc4c8188a158793d8702c0e29397ce254
1 //-----------------------------------------------------------------------
2 // <copyright file="SigningBindingElement.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.OpenId.ChannelElements {
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Globalization;
12 using System.Linq;
13 using System.Net.Security;
14 using DotNetOpenAuth.Loggers;
15 using DotNetOpenAuth.Messaging;
16 using DotNetOpenAuth.Messaging.Bindings;
17 using DotNetOpenAuth.Messaging.Reflection;
18 using DotNetOpenAuth.OpenId.Messages;
19 using DotNetOpenAuth.OpenId.Provider;
20 using DotNetOpenAuth.OpenId.RelyingParty;
22 /// <summary>
23 /// Signs and verifies authentication assertions.
24 /// </summary>
25 internal class SigningBindingElement : IChannelBindingElement {
26 /// <summary>
27 /// The association store used by Relying Parties to look up the secrets needed for signing.
28 /// </summary>
29 private readonly IAssociationStore<Uri> rpAssociations;
31 /// <summary>
32 /// The association store used by Providers to look up the secrets needed for signing.
33 /// </summary>
34 private readonly IAssociationStore<AssociationRelyingPartyType> opAssociations;
36 /// <summary>
37 /// The security settings at the Provider.
38 /// Only defined when this element is instantiated to service a Provider.
39 /// </summary>
40 private readonly ProviderSecuritySettings opSecuritySettings;
42 /// <summary>
43 /// A logger specifically used for logging verbose text on everything about the signing process.
44 /// </summary>
45 private static ILog signingLogger = Logger.Create(typeof(SigningBindingElement));
47 /// <summary>
48 /// Initializes a new instance of the SigningBindingElement class for use by a Relying Party.
49 /// </summary>
50 /// <param name="associationStore">The association store used to look up the secrets needed for signing. May be null for dumb Relying Parties.</param>
51 internal SigningBindingElement(IAssociationStore<Uri> associationStore) {
52 this.rpAssociations = associationStore;
55 /// <summary>
56 /// Initializes a new instance of the SigningBindingElement class for use by a Provider.
57 /// </summary>
58 /// <param name="associationStore">The association store used to look up the secrets needed for signing.</param>
59 /// <param name="securitySettings">The security settings.</param>
60 internal SigningBindingElement(IAssociationStore<AssociationRelyingPartyType> associationStore, ProviderSecuritySettings securitySettings) {
61 ErrorUtilities.VerifyArgumentNotNull(associationStore, "associationStore");
62 ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings");
64 this.opAssociations = associationStore;
65 this.opSecuritySettings = securitySettings;
68 #region IChannelBindingElement Properties
70 /// <summary>
71 /// Gets the protection offered (if any) by this binding element.
72 /// </summary>
73 /// <value><see cref="MessageProtections.TamperProtection"/></value>
74 public MessageProtections Protection {
75 get { return MessageProtections.TamperProtection; }
78 /// <summary>
79 /// Gets or sets the channel that this binding element belongs to.
80 /// </summary>
81 public Channel Channel { get; set; }
83 #endregion
85 /// <summary>
86 /// Gets a value indicating whether this binding element is on a Provider channel.
87 /// </summary>
88 private bool IsOnProvider {
89 get { return this.opAssociations != null; }
92 #region IChannelBindingElement Methods
94 /// <summary>
95 /// Prepares a message for sending based on the rules of this channel binding element.
96 /// </summary>
97 /// <param name="message">The message to prepare for sending.</param>
98 /// <returns>
99 /// True if the <paramref name="message"/> applied to this binding element
100 /// and the operation was successful. False otherwise.
101 /// </returns>
102 public bool PrepareMessageForSending(IProtocolMessage message) {
103 var signedMessage = message as ITamperResistantOpenIdMessage;
104 if (signedMessage != null) {
105 Logger.DebugFormat("Signing {0} message.", message.GetType().Name);
106 Association association = this.GetAssociation(signedMessage);
107 signedMessage.AssociationHandle = association.Handle;
108 signedMessage.SignedParameterOrder = this.GetSignedParameterOrder(signedMessage);
109 signedMessage.Signature = GetSignature(signedMessage, association);
110 return true;
113 return false;
116 /// <summary>
117 /// Performs any transformation on an incoming message that may be necessary and/or
118 /// validates an incoming message based on the rules of this channel binding element.
119 /// </summary>
120 /// <param name="message">The incoming message to process.</param>
121 /// <returns>
122 /// True if the <paramref name="message"/> applied to this binding element
123 /// and the operation was successful. False if the operation did not apply to this message.
124 /// </returns>
125 /// <exception cref="ProtocolException">
126 /// Thrown when the binding element rules indicate that this message is invalid and should
127 /// NOT be processed.
128 /// </exception>
129 public bool PrepareMessageForReceiving(IProtocolMessage message) {
130 var signedMessage = message as ITamperResistantOpenIdMessage;
131 if (signedMessage != null) {
132 Logger.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature);
134 EnsureParametersRequiringSignatureAreSigned(signedMessage);
136 Association association = this.GetSpecificAssociation(signedMessage);
137 if (association != null) {
138 string signature = GetSignature(signedMessage, association);
139 if (!string.Equals(signedMessage.Signature, signature, StringComparison.Ordinal)) {
140 Logger.Error("Signature verification failed.");
141 throw new InvalidSignatureException(message);
143 } else {
144 ErrorUtilities.VerifyInternal(this.Channel != null, "Cannot verify private association signature because we don't have a channel.");
146 // We did not recognize the association the provider used to sign the message.
147 // Ask the provider to check the signature then.
148 var indirectSignedResponse = (IndirectSignedResponse)signedMessage;
149 var checkSignatureRequest = new CheckAuthenticationRequest(indirectSignedResponse);
150 var checkSignatureResponse = this.Channel.Request<CheckAuthenticationResponse>(checkSignatureRequest);
151 if (!checkSignatureResponse.IsValid) {
152 Logger.Error("Provider reports signature verification failed.");
153 throw new InvalidSignatureException(message);
156 // If the OP confirms that a handle should be invalidated as well, do that.
157 if (!string.IsNullOrEmpty(checkSignatureResponse.InvalidateHandle)) {
158 if (this.rpAssociations != null) {
159 this.rpAssociations.RemoveAssociation(indirectSignedResponse.ProviderEndpoint, checkSignatureResponse.InvalidateHandle);
164 return true;
167 return false;
170 #endregion
172 /// <summary>
173 /// Ensures that all message parameters that must be signed are in fact included
174 /// in the signature.
175 /// </summary>
176 /// <param name="signedMessage">The signed message.</param>
177 private static void EnsureParametersRequiringSignatureAreSigned(ITamperResistantOpenIdMessage signedMessage) {
178 // Verify that the signed parameter order includes the mandated fields.
179 // We do this in such a way that derived classes that add mandated fields automatically
180 // get included in the list of checked parameters.
181 Protocol protocol = Protocol.Lookup(signedMessage.Version);
182 var partsRequiringProtection = from part in MessageDescription.Get(signedMessage.GetType(), signedMessage.Version).Mapping.Values
183 where part.RequiredProtection != ProtectionLevel.None
184 select part.Name;
185 ErrorUtilities.VerifyInternal(partsRequiringProtection.All(name => name.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)), "Signing only works when the parameters start with the 'openid.' prefix.");
186 string[] signedParts = signedMessage.SignedParameterOrder.Split(',');
187 var unsignedParts = from partName in partsRequiringProtection
188 where !signedParts.Contains(partName.Substring(protocol.openid.Prefix.Length))
189 select partName;
190 ErrorUtilities.VerifyProtocol(!unsignedParts.Any(), OpenIdStrings.SignatureDoesNotIncludeMandatoryParts, string.Join(", ", unsignedParts.ToArray()));
193 /// <summary>
194 /// Calculates the signature for a given message.
195 /// </summary>
196 /// <param name="signedMessage">The message to sign or verify.</param>
197 /// <param name="association">The association to use to sign the message.</param>
198 /// <returns>The calculated signature of the method.</returns>
199 private static string GetSignature(ITamperResistantOpenIdMessage signedMessage, Association association) {
200 ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage");
201 ErrorUtilities.VerifyNonZeroLength(signedMessage.SignedParameterOrder, "signedMessage.SignedParameterOrder");
202 ErrorUtilities.VerifyArgumentNotNull(association, "association");
204 // Prepare the parts to sign, taking care to replace an openid.mode value
205 // of check_authentication with its original id_res so the signature matches.
206 Protocol protocol = Protocol.Lookup(signedMessage.Version);
207 MessageDictionary dictionary = new MessageDictionary(signedMessage);
208 var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',')
209 let prefixedName = Protocol.V20.openid.Prefix + name
210 select new KeyValuePair<string, string>(name, dictionary[prefixedName]);
212 byte[] dataToSign = KeyValueFormEncoding.GetBytes(parametersToSign);
213 string signature = Convert.ToBase64String(association.Sign(dataToSign));
215 if (signingLogger.IsDebugEnabled) {
216 signingLogger.DebugFormat(
217 CultureInfo.InvariantCulture,
218 "Signing these message parts: {0}{1}{0}Base64 representation of signed data: {2}{0}Signature: {3}",
219 Environment.NewLine,
220 parametersToSign.ToStringDeferred(),
221 Convert.ToBase64String(dataToSign),
222 signature);
225 return signature;
228 /// <summary>
229 /// Gets the value to use for the openid.signed parameter.
230 /// </summary>
231 /// <param name="signedMessage">The signable message.</param>
232 /// <returns>
233 /// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines
234 /// the inclusion and order of message parts that will be signed.
235 /// </returns>
236 private string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) {
237 ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage");
239 Protocol protocol = Protocol.Lookup(signedMessage.Version);
241 MessageDescription description = MessageDescription.Get(signedMessage.GetType(), signedMessage.Version);
242 var signedParts = from part in description.Mapping.Values
243 where (part.RequiredProtection & System.Net.Security.ProtectionLevel.Sign) != 0
244 && part.GetValue(signedMessage) != null
245 select part.Name;
246 string prefix = Protocol.V20.openid.Prefix;
247 ErrorUtilities.VerifyInternal(signedParts.All(name => name.StartsWith(prefix, StringComparison.Ordinal)), "All signed message parts must start with 'openid.'.");
249 if (this.opSecuritySettings.SignOutgoingExtensions) {
250 // Tack on any ExtraData parameters that start with 'openid.'.
251 List<string> extraSignedParameters = new List<string>(signedMessage.ExtraData.Count);
252 foreach (string key in signedMessage.ExtraData.Keys) {
253 if (key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)) {
254 extraSignedParameters.Add(key);
255 } else {
256 Logger.DebugFormat("The extra parameter '{0}' will not be signed because it does not start with 'openid.'.", key);
259 signedParts = signedParts.Concat(extraSignedParameters);
262 int skipLength = prefix.Length;
263 string signedFields = string.Join(",", signedParts.Select(name => name.Substring(skipLength)).ToArray());
264 return signedFields;
267 /// <summary>
268 /// Gets the association to use to sign or verify a message.
269 /// </summary>
270 /// <param name="signedMessage">The message to sign or verify.</param>
271 /// <returns>The association to use to sign or verify the message.</returns>
272 private Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) {
273 if (this.IsOnProvider) {
274 // We're on a Provider to either sign (smart/dumb) or verify a dumb signature.
275 return this.GetSpecificAssociation(signedMessage) ?? this.GetDumbAssociationForSigning();
276 } else {
277 // We're on a Relying Party verifying a signature.
278 IDirectedProtocolMessage directedMessage = (IDirectedProtocolMessage)signedMessage;
279 if (this.rpAssociations != null) {
280 return this.rpAssociations.GetAssociation(directedMessage.Recipient, signedMessage.AssociationHandle);
281 } else {
282 return null;
287 /// <summary>
288 /// Gets a specific association referenced in a given message's association handle.
289 /// </summary>
290 /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param>
291 /// <returns>The referenced association; or <c>null</c> if such an association cannot be found.</returns>
292 /// <remarks>
293 /// If the association handle set in the message does not match any valid association,
294 /// the association handle property is cleared, and the
295 /// <see cref="ITamperResistantOpenIdMessage.InvalidateHandle"/> property is set to the
296 /// handle that could not be found.
297 /// </remarks>
298 private Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) {
299 Association association = null;
301 if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) {
302 IndirectSignedResponse indirectSignedMessage = signedMessage as IndirectSignedResponse;
303 if (this.IsOnProvider) {
304 // Since we have an association handle, we're either signing with a smart association,
305 // or verifying a dumb one.
306 bool signing = string.IsNullOrEmpty(signedMessage.Signature);
307 ErrorUtilities.VerifyInternal(signing == (indirectSignedMessage != null), "Ooops... somehow we think we're signing a message that isn't a signed response!");
308 AssociationRelyingPartyType type = signing ? AssociationRelyingPartyType.Smart : AssociationRelyingPartyType.Dumb;
309 association = this.opAssociations.GetAssociation(type, signedMessage.AssociationHandle);
310 if (association == null) {
311 // There was no valid association with the requested handle.
312 // Let's tell the RP to forget about that association.
313 signedMessage.InvalidateHandle = signedMessage.AssociationHandle;
314 signedMessage.AssociationHandle = null;
316 } else if (this.rpAssociations != null) { // if on a smart RP
317 Uri providerEndpoint = indirectSignedMessage.ProviderEndpoint;
318 association = this.rpAssociations.GetAssociation(providerEndpoint, signedMessage.AssociationHandle);
322 return association;
325 /// <summary>
326 /// Gets a private Provider association used for signing messages in "dumb" mode.
327 /// </summary>
328 /// <returns>An existing or newly created association.</returns>
329 private Association GetDumbAssociationForSigning() {
330 // If no assoc_handle was given or it was invalid, the only thing
331 // left to do is sign a message using a 'dumb' mode association.
332 Protocol protocol = Protocol.Default;
333 Association association = this.opAssociations.GetAssociation(AssociationRelyingPartyType.Dumb);
334 if (association == null) {
335 association = HmacShaAssociation.Create(protocol, protocol.Args.SignatureAlgorithm.HMAC_SHA256, AssociationRelyingPartyType.Dumb, this.opSecuritySettings);
336 this.opAssociations.StoreAssociation(AssociationRelyingPartyType.Dumb, association);
339 return association;