1
//-----------------------------------------------------------------------
2 // <copyright file="SigningBindingElement.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
.Diagnostics
;
11 using System
.Globalization
;
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
;
23 /// Signs and verifies authentication assertions.
25 internal class SigningBindingElement
: IChannelBindingElement
{
27 /// The association store used by Relying Parties to look up the secrets needed for signing.
29 private readonly IAssociationStore
<Uri
> rpAssociations
;
32 /// The association store used by Providers to look up the secrets needed for signing.
34 private readonly IAssociationStore
<AssociationRelyingPartyType
> opAssociations
;
37 /// The security settings at the Provider.
38 /// Only defined when this element is instantiated to service a Provider.
40 private readonly ProviderSecuritySettings opSecuritySettings
;
43 /// A logger specifically used for logging verbose text on everything about the signing process.
45 private static ILog signingLogger
= Logger
.Create(typeof(SigningBindingElement
));
48 /// Initializes a new instance of the SigningBindingElement class for use by a Relying Party.
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
;
56 /// Initializes a new instance of the SigningBindingElement class for use by a Provider.
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
71 /// Gets the protection offered (if any) by this binding element.
73 /// <value><see cref="MessageProtections.TamperProtection"/></value>
74 public MessageProtections Protection
{
75 get { return MessageProtections.TamperProtection; }
79 /// Gets or sets the channel that this binding element belongs to.
81 public Channel Channel { get; set; }
86 /// Gets a value indicating whether this binding element is on a Provider channel.
88 private bool IsOnProvider
{
89 get { return this.opAssociations != null; }
92 #region IChannelBindingElement Methods
95 /// Prepares a message for sending based on the rules of this channel binding element.
97 /// <param name="message">The message to prepare for sending.</param>
99 /// True if the <paramref name="message"/> applied to this binding element
100 /// and the operation was successful. False otherwise.
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
);
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.
120 /// <param name="message">The incoming message to process.</param>
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.
125 /// <exception cref="ProtocolException">
126 /// Thrown when the binding element rules indicate that this message is invalid and should
127 /// NOT be processed.
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
);
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
);
173 /// Ensures that all message parameters that must be signed are in fact included
174 /// in the signature.
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
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
))
190 ErrorUtilities
.VerifyProtocol(!unsignedParts
.Any(), OpenIdStrings
.SignatureDoesNotIncludeMandatoryParts
, string.Join(", ", unsignedParts
.ToArray()));
194 /// Calculates the signature for a given message.
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}",
220 parametersToSign
.ToStringDeferred(),
221 Convert
.ToBase64String(dataToSign
),
229 /// Gets the value to use for the openid.signed parameter.
231 /// <param name="signedMessage">The signable message.</param>
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.
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
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
);
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());
268 /// Gets the association to use to sign or verify a message.
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();
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
);
288 /// Gets a specific association referenced in a given message's association handle.
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>
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.
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
);
326 /// Gets a private Provider association used for signing messages in "dumb" mode.
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
);