Individual association types can now have configured lifetimes.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / ChannelElements / SigningBindingElement.cs
blobb3acc936c9b8cc054dc73ec5221239f3d1a80df4
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.Linq;
12 using System.Net.Security;
13 using DotNetOpenAuth.Messaging;
14 using DotNetOpenAuth.Messaging.Bindings;
15 using DotNetOpenAuth.Messaging.Reflection;
16 using DotNetOpenAuth.OpenId.Messages;
17 using DotNetOpenAuth.OpenId.Provider;
18 using DotNetOpenAuth.OpenId.RelyingParty;
20 /// <summary>
21 /// Signs and verifies authentication assertions.
22 /// </summary>
23 internal class SigningBindingElement : IChannelBindingElement {
24 /// <summary>
25 /// The association store used by Relying Parties to look up the secrets needed for signing.
26 /// </summary>
27 private readonly IAssociationStore<Uri> rpAssociations;
29 /// <summary>
30 /// The association store used by Providers to look up the secrets needed for signing.
31 /// </summary>
32 private readonly IAssociationStore<AssociationRelyingPartyType> opAssociations;
34 /// <summary>
35 /// The security settings at the Provider.
36 /// Only defined when this element is instantiated to service a Provider.
37 /// </summary>
38 private readonly ProviderSecuritySettings opSecuritySettings;
40 /// <summary>
41 /// Initializes a new instance of the SigningBindingElement class for use by a Relying Party.
42 /// </summary>
43 /// <param name="associationStore">The association store used to look up the secrets needed for signing. May be null for dumb Relying Parties.</param>
44 internal SigningBindingElement(IAssociationStore<Uri> associationStore) {
45 this.rpAssociations = associationStore;
48 /// <summary>
49 /// Initializes a new instance of the SigningBindingElement class for use by a Provider.
50 /// </summary>
51 /// <param name="associationStore">The association store used to look up the secrets needed for signing.</param>
52 /// <param name="securitySettings">The security settings.</param>
53 internal SigningBindingElement(IAssociationStore<AssociationRelyingPartyType> associationStore, ProviderSecuritySettings securitySettings) {
54 ErrorUtilities.VerifyArgumentNotNull(associationStore, "associationStore");
55 ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings");
57 this.opAssociations = associationStore;
58 this.opSecuritySettings = securitySettings;
61 #region IChannelBindingElement Properties
63 /// <summary>
64 /// Gets the protection offered (if any) by this binding element.
65 /// </summary>
66 /// <value><see cref="MessageProtections.TamperProtection"/></value>
67 public MessageProtections Protection {
68 get { return MessageProtections.TamperProtection; }
71 /// <summary>
72 /// Gets or sets the channel that this binding element belongs to.
73 /// </summary>
74 public Channel Channel { get; set; }
76 #endregion
78 /// <summary>
79 /// Gets a value indicating whether this binding element is on a Provider channel.
80 /// </summary>
81 private bool IsOnProvider {
82 get { return this.opAssociations != null; }
85 #region IChannelBindingElement Methods
87 /// <summary>
88 /// Prepares a message for sending based on the rules of this channel binding element.
89 /// </summary>
90 /// <param name="message">The message to prepare for sending.</param>
91 /// <returns>
92 /// True if the <paramref name="message"/> applied to this binding element
93 /// and the operation was successful. False otherwise.
94 /// </returns>
95 public bool PrepareMessageForSending(IProtocolMessage message) {
96 var signedMessage = message as ITamperResistantOpenIdMessage;
97 if (signedMessage != null) {
98 Logger.DebugFormat("Signing {0} message.", message.GetType().Name);
99 Association association = this.GetAssociation(signedMessage);
100 signedMessage.AssociationHandle = association.Handle;
101 signedMessage.SignedParameterOrder = GetSignedParameterOrder(signedMessage);
102 signedMessage.Signature = this.GetSignature(signedMessage, association);
103 return true;
106 return false;
109 /// <summary>
110 /// Performs any transformation on an incoming message that may be necessary and/or
111 /// validates an incoming message based on the rules of this channel binding element.
112 /// </summary>
113 /// <param name="message">The incoming message to process.</param>
114 /// <returns>
115 /// True if the <paramref name="message"/> applied to this binding element
116 /// and the operation was successful. False if the operation did not apply to this message.
117 /// </returns>
118 /// <exception cref="ProtocolException">
119 /// Thrown when the binding element rules indicate that this message is invalid and should
120 /// NOT be processed.
121 /// </exception>
122 public bool PrepareMessageForReceiving(IProtocolMessage message) {
123 var signedMessage = message as ITamperResistantOpenIdMessage;
124 if (signedMessage != null) {
125 Logger.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature);
127 EnsureParametersRequiringSignatureAreSigned(signedMessage);
129 Association association = this.GetSpecificAssociation(signedMessage);
130 if (association != null) {
131 string signature = this.GetSignature(signedMessage, association);
132 if (!string.Equals(signedMessage.Signature, signature, StringComparison.Ordinal)) {
133 Logger.Error("Signature verification failed.");
134 throw new InvalidSignatureException(message);
136 } else {
137 ErrorUtilities.VerifyInternal(this.Channel != null, "Cannot verify private association signature because we don't have a channel.");
139 // We did not recognize the association the provider used to sign the message.
140 // Ask the provider to check the signature then.
141 var indirectSignedResponse = (IndirectSignedResponse)signedMessage;
142 var checkSignatureRequest = new CheckAuthenticationRequest(indirectSignedResponse);
143 var checkSignatureResponse = this.Channel.Request<CheckAuthenticationResponse>(checkSignatureRequest);
144 if (!checkSignatureResponse.IsValid) {
145 Logger.Error("Provider reports signature verification failed.");
146 throw new InvalidSignatureException(message);
149 // If the OP confirms that a handle should be invalidated as well, do that.
150 if (!string.IsNullOrEmpty(checkSignatureResponse.InvalidateHandle)) {
151 if (this.rpAssociations != null) {
152 this.rpAssociations.RemoveAssociation(indirectSignedResponse.ProviderEndpoint, checkSignatureResponse.InvalidateHandle);
157 return true;
160 return false;
163 #endregion
165 /// <summary>
166 /// Ensures that all message parameters that must be signed are in fact included
167 /// in the signature.
168 /// </summary>
169 /// <param name="signedMessage">The signed message.</param>
170 private static void EnsureParametersRequiringSignatureAreSigned(ITamperResistantOpenIdMessage signedMessage) {
171 // Verify that the signed parameter order includes the mandated fields.
172 // We do this in such a way that derived classes that add mandated fields automatically
173 // get included in the list of checked parameters.
174 Protocol protocol = Protocol.Lookup(signedMessage.Version);
175 var partsRequiringProtection = from part in MessageDescription.Get(signedMessage.GetType(), signedMessage.Version).Mapping.Values
176 where part.RequiredProtection != ProtectionLevel.None
177 select part.Name;
178 ErrorUtilities.VerifyInternal(partsRequiringProtection.All(name => name.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)), "Signing only works when the parameters start with the 'openid.' prefix.");
179 string[] signedParts = signedMessage.SignedParameterOrder.Split(',');
180 var unsignedParts = from partName in partsRequiringProtection
181 where !signedParts.Contains(partName.Substring(protocol.openid.Prefix.Length))
182 select partName;
183 ErrorUtilities.VerifyProtocol(!unsignedParts.Any(), OpenIdStrings.SignatureDoesNotIncludeMandatoryParts, string.Join(", ", unsignedParts.ToArray()));
186 /// <summary>
187 /// Gets the value to use for the openid.signed parameter.
188 /// </summary>
189 /// <param name="signedMessage">The signable message.</param>
190 /// <returns>
191 /// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines
192 /// the inclusion and order of message parts that will be signed.
193 /// </returns>
194 private static string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) {
195 ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage");
197 MessageDescription description = MessageDescription.Get(signedMessage.GetType(), signedMessage.Version);
198 var signedParts = from part in description.Mapping.Values
199 where (part.RequiredProtection & System.Net.Security.ProtectionLevel.Sign) != 0
200 && part.GetValue(signedMessage) != null
201 select part.Name;
202 string prefix = Protocol.V20.openid.Prefix;
203 Debug.Assert(signedParts.All(name => name.StartsWith(prefix, StringComparison.Ordinal)), "All signed message parts must start with 'openid.'.");
204 int skipLength = prefix.Length;
205 string signedFields = string.Join(",", signedParts.Select(name => name.Substring(skipLength)).ToArray());
206 return signedFields;
209 /// <summary>
210 /// Calculates the signature for a given message.
211 /// </summary>
212 /// <param name="signedMessage">The message to sign or verify.</param>
213 /// <param name="association">The association to use to sign the message.</param>
214 /// <returns>The calculated signature of the method.</returns>
215 private string GetSignature(ITamperResistantOpenIdMessage signedMessage, Association association) {
216 ErrorUtilities.VerifyArgumentNotNull(signedMessage, "signedMessage");
217 ErrorUtilities.VerifyNonZeroLength(signedMessage.SignedParameterOrder, "signedMessage.SignedParameterOrder");
218 ErrorUtilities.VerifyArgumentNotNull(association, "association");
220 // Prepare the parts to sign, taking care to replace an openid.mode value
221 // of check_authentication with its original id_res so the signature matches.
222 Protocol protocol = Protocol.Lookup(signedMessage.Version);
223 MessageDictionary dictionary = new MessageDictionary(signedMessage);
224 var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',')
225 let prefixedName = Protocol.V20.openid.Prefix + name
226 select new KeyValuePair<string, string>(prefixedName, dictionary[prefixedName]);
228 byte[] dataToSign = KeyValueFormEncoding.GetBytes(parametersToSign);
229 return Convert.ToBase64String(association.Sign(dataToSign));
232 /// <summary>
233 /// Gets the association to use to sign or verify a message.
234 /// </summary>
235 /// <param name="signedMessage">The message to sign or verify.</param>
236 /// <returns>The association to use to sign or verify the message.</returns>
237 private Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) {
238 if (this.IsOnProvider) {
239 // We're on a Provider to either sign (smart/dumb) or verify a dumb signature.
240 return this.GetSpecificAssociation(signedMessage) ?? this.GetDumbAssociationForSigning();
241 } else {
242 // We're on a Relying Party verifying a signature.
243 IDirectedProtocolMessage directedMessage = (IDirectedProtocolMessage)signedMessage;
244 if (this.rpAssociations != null) {
245 return this.rpAssociations.GetAssociation(directedMessage.Recipient, signedMessage.AssociationHandle);
246 } else {
247 return null;
252 /// <summary>
253 /// Gets a specific association referenced in a given message's association handle.
254 /// </summary>
255 /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param>
256 /// <returns>The referenced association; or <c>null</c> if such an association cannot be found.</returns>
257 /// <remarks>
258 /// If the association handle set in the message does not match any valid association,
259 /// the association handle property is cleared, and the
260 /// <see cref="ITamperResistantOpenIdMessage.InvalidateHandle"/> property is set to the
261 /// handle that could not be found.
262 /// </remarks>
263 private Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) {
264 Association association = null;
266 if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) {
267 if (this.IsOnProvider) {
268 // Since we have an association handle, we're either signing with a smart association,
269 // or verifying a dumb one.
270 bool signing = string.IsNullOrEmpty(signedMessage.Signature);
271 ErrorUtilities.VerifyInternal(signing == (signedMessage is PositiveAssertionResponse), "Ooops... somehow we think we're signing a message that isn't a positive assertion!");
272 AssociationRelyingPartyType type = signing ? AssociationRelyingPartyType.Smart : AssociationRelyingPartyType.Dumb;
273 association = this.opAssociations.GetAssociation(type, signedMessage.AssociationHandle);
274 if (association == null) {
275 // There was no valid association with the requested handle.
276 // Let's tell the RP to forget about that association.
277 signedMessage.InvalidateHandle = signedMessage.AssociationHandle;
278 signedMessage.AssociationHandle = null;
280 } else if (this.rpAssociations != null) { // if on a smart RP
281 Uri providerEndpoint = ((PositiveAssertionResponse)signedMessage).ProviderEndpoint;
282 association = this.rpAssociations.GetAssociation(providerEndpoint, signedMessage.AssociationHandle);
286 return association;
289 /// <summary>
290 /// Gets a private Provider association used for signing messages in "dumb" mode.
291 /// </summary>
292 /// <returns>An existing or newly created association.</returns>
293 private Association GetDumbAssociationForSigning() {
294 // If no assoc_handle was given or it was invalid, the only thing
295 // left to do is sign a message using a 'dumb' mode association.
296 Protocol protocol = Protocol.Default;
297 Association association = this.opAssociations.GetAssociation(AssociationRelyingPartyType.Dumb);
298 if (association == null) {
299 association = HmacShaAssociation.Create(protocol, protocol.Args.SignatureAlgorithm.HMAC_SHA256, AssociationRelyingPartyType.Dumb, this.opSecuritySettings);
300 this.opAssociations.StoreAssociation(AssociationRelyingPartyType.Dumb, association);
303 return association;