//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.ChannelElements { using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.Messages; using Validation; /// /// 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. /// /// /// 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. /// 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. /// internal class ReturnToSignatureBindingElement : IChannelBindingElement { /// /// A reusable pre-completed task that may be returned multiple times to reduce GC pressure. /// private static readonly Task NullTask = Task.FromResult(null); /// /// A reusable pre-completed task that may be returned multiple times to reduce GC pressure. /// private static readonly Task NoneTask = Task.FromResult(MessageProtections.None); /// /// The name of the callback parameter we'll tack onto the return_to value /// to store our signature on the return_to parameter. /// private const string ReturnToSignatureParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig"; /// /// 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. /// private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle"; /// /// The URI to use for private associations at this RP. /// private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret"); /// /// The key store used to generate the private signature on the return_to parameter. /// private ICryptoKeyStore cryptoKeyStore; /// /// Initializes a new instance of the class. /// /// The crypto key store. internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) { Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); this.cryptoKeyStore = cryptoKeyStore; } #region IChannelBindingElement Members /// /// Gets or sets the channel that this binding element belongs to. /// /// /// /// This property is set by the channel when it is first constructed. /// public Channel Channel { get; set; } /// /// Gets the protection offered (if any) by this binding element. /// /// /// /// No message protection is reported because this binding element /// does not protect the entire message -- only a part. /// public MessageProtections Protection { get { return MessageProtections.None; } } /// /// Prepares a message for sending based on the rules of this channel binding element. /// /// The message to prepare for sending. /// The cancellation token. /// /// 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. /// /// /// Implementations that provide message protection must honor the /// properties where applicable. /// public Task ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { 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 NoneTask; } return NullTask; } /// /// 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. /// /// The incoming message to process. /// The cancellation token. /// /// 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. /// /// /// Thrown when the binding element rules indicate that this message is invalid and should /// NOT be processed. /// /// /// Implementations that provide message protection must honor the /// properties where applicable. /// public Task ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { 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 NoneTask; } } return NullTask; } #endregion /// /// Gets the return to signature. /// /// The return to. /// The crypto key. /// /// The generated signature. /// /// /// 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. /// 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(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]); ErrorUtilities.VerifyProtocol( cryptoKey != null, MessagingStrings.MissingDecryptionKeyForHandle, SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]); } using (var signer = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, cryptoKey.Key)) { signature = signer.ComputeHash(bytesToSign); } } catch (ProtocolException ex) { throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired); } return signature; } } }