//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.ChannelElements {
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.Messaging.Reflection;
using DotNetOpenAuth.OpenId.Messages;
using DotNetOpenAuth.OpenId.Provider;
using Validation;
///
/// The signing binding element for OpenID Providers.
///
internal class ProviderSigningBindingElement : SigningBindingElement {
///
/// The association store used by Providers to look up the secrets needed for signing.
///
private readonly IProviderAssociationStore opAssociations;
///
/// The security settings at the Provider.
/// Only defined when this element is instantiated to service a Provider.
///
private readonly ProviderSecuritySettings opSecuritySettings;
///
/// Initializes a new instance of the class.
///
/// The association store used to look up the secrets needed for signing.
/// The security settings.
internal ProviderSigningBindingElement(IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) {
Requires.NotNull(associationStore, "associationStore");
Requires.NotNull(securitySettings, "securitySettings");
this.opAssociations = associationStore;
this.opSecuritySettings = securitySettings;
}
///
/// Gets a value indicating whether this binding element is on a Provider channel.
///
protected override bool IsOnProvider {
get { return true; }
}
///
/// 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.
///
public override async Task ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) {
var result = await base.ProcessOutgoingMessageAsync(message, cancellationToken);
if (result != null) {
return result;
}
var signedMessage = message as ITamperResistantOpenIdMessage;
if (signedMessage != null) {
Logger.Bindings.DebugFormat("Signing {0} message.", message.GetType().Name);
Association association = this.GetAssociation(signedMessage);
signedMessage.AssociationHandle = association.Handle;
signedMessage.SignedParameterOrder = this.GetSignedParameterOrder(signedMessage);
signedMessage.Signature = this.GetSignature(signedMessage, association);
return MessageProtections.TamperProtection;
}
return null;
}
///
/// Gets the association to use to sign or verify a message.
///
/// The message to sign or verify.
///
/// The association to use to sign or verify the message.
///
protected override Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) {
// We're on a Provider to either sign (smart/dumb) or verify a dumb signature.
bool signing = string.IsNullOrEmpty(signedMessage.Signature);
if (signing) {
// If the RP has no replay protection, coerce use of a private association
// instead of a shared one (if security settings indicate)
// to protect the authenticating user from replay attacks.
bool forcePrivateAssociation = this.opSecuritySettings.ProtectDownlevelReplayAttacks
&& IsRelyingPartyVulnerableToReplays(null, (IndirectSignedResponse)signedMessage);
if (forcePrivateAssociation) {
if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) {
Logger.Signatures.Info("An OpenID 1.x authentication request with a shared association handle will be responded to with a private association in order to provide OP-side replay protection.");
}
return this.GetDumbAssociationForSigning();
} else {
return this.GetSpecificAssociation(signedMessage) ?? this.GetDumbAssociationForSigning();
}
} else {
return this.GetSpecificAssociation(signedMessage);
}
}
///
/// Gets a specific association referenced in a given message's association handle.
///
/// The signed message whose association handle should be used to lookup the association to return.
///
/// The referenced association; or null if such an association cannot be found.
///
protected override Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) {
Association association = null;
if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) {
IndirectSignedResponse indirectSignedMessage = signedMessage as IndirectSignedResponse;
// Since we have an association handle, we're either signing with a smart association,
// or verifying a dumb one.
bool signing = string.IsNullOrEmpty(signedMessage.Signature);
bool isPrivateAssociation = !signing;
association = this.opAssociations.Deserialize(signedMessage, isPrivateAssociation, signedMessage.AssociationHandle);
if (association == null) {
// There was no valid association with the requested handle.
// Let's tell the RP to forget about that association.
signedMessage.InvalidateHandle = signedMessage.AssociationHandle;
signedMessage.AssociationHandle = null;
}
}
return association;
}
///
/// Gets a private Provider association used for signing messages in "dumb" mode.
///
/// An existing or newly created association.
protected override Association GetDumbAssociationForSigning() {
// If no assoc_handle was given or it was invalid, the only thing
// left to do is sign a message using a 'dumb' mode association.
Protocol protocol = Protocol.Default;
Association association = HmacShaAssociationProvider.Create(protocol, protocol.Args.SignatureAlgorithm.HMAC_SHA256, AssociationRelyingPartyType.Dumb, this.opAssociations, this.opSecuritySettings);
return association;
}
///
/// Verifies the signature by unrecognized handle.
///
/// The message.
/// The signed message.
/// The protections applied.
/// The cancellation token.
///
/// The applied protections.
///
protected override Task VerifySignatureByUnrecognizedHandleAsync(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied, CancellationToken cancellationToken) {
// If we're on the Provider, then the RP sent us a check_auth with a signature
// we don't have an association for. (It may have expired, or it may be a faulty RP).
var tcs = new TaskCompletionSource();
tcs.SetException(new InvalidSignatureException(message));
return tcs.Task;
}
///
/// Determines whether the relying party sending an authentication request is
/// vulnerable to replay attacks.
///
/// The request message from the Relying Party. Useful, but may be null for conservative estimate results.
/// The response message to be signed.
///
/// true if the relying party is vulnerable; otherwise, false.
///
private static bool IsRelyingPartyVulnerableToReplays(SignedResponseRequest request, IndirectSignedResponse response) {
Requires.NotNull(response, "response");
// OpenID 2.0 includes replay protection as part of the protocol.
if (response.Version.Major >= 2) {
return false;
}
// This library's RP may be on the remote end, and may be using 1.x merely because
// discovery on the Claimed Identifier suggested this was a 1.x OP.
// Since this library's RP has a built-in request_nonce parameter for replay
// protection, we'll allow for that.
var returnToArgs = HttpUtility.ParseQueryString(response.ReturnTo.Query);
if (!string.IsNullOrEmpty(returnToArgs[Protocol.ReturnToNonceParameter])) {
return false;
}
// If the OP endpoint _AND_ RP return_to URL uses HTTPS then no one
// can steal and replay the positive assertion.
// We can only ascertain this if the request message was handed to us
// so we know what our own OP endpoint is. If we don't have a request
// message, then we'll default to assuming it's insecure.
if (request != null) {
if (request.Recipient.IsTransportSecure() && response.Recipient.IsTransportSecure()) {
return false;
}
}
// Nothing left to protect against replays. RP is vulnerable.
return true;
}
///
/// Gets the value to use for the openid.signed parameter.
///
/// The signable message.
///
/// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines
/// the inclusion and order of message parts that will be signed.
///
private string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) {
RequiresEx.ValidState(this.Channel != null);
Requires.NotNull(signedMessage, "signedMessage");
Protocol protocol = Protocol.Lookup(signedMessage.Version);
MessageDescription description = this.Channel.MessageDescriptions.Get(signedMessage);
var signedParts = from part in description.Mapping.Values
where (part.RequiredProtection & System.Net.Security.ProtectionLevel.Sign) != 0
&& part.GetValue(signedMessage) != null
select part.Name;
string prefix = Protocol.V20.openid.Prefix;
ErrorUtilities.VerifyInternal(signedParts.All(name => name.StartsWith(prefix, StringComparison.Ordinal)), "All signed message parts must start with 'openid.'.");
if (this.opSecuritySettings.SignOutgoingExtensions) {
// Tack on any ExtraData parameters that start with 'openid.'.
List extraSignedParameters = new List(signedMessage.ExtraData.Count);
foreach (string key in signedMessage.ExtraData.Keys) {
if (key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)) {
extraSignedParameters.Add(key);
} else {
Logger.Signatures.DebugFormat("The extra parameter '{0}' will not be signed because it does not start with 'openid.'.", key);
}
}
signedParts = signedParts.Concat(extraSignedParameters);
}
int skipLength = prefix.Length;
string signedFields = string.Join(",", signedParts.Select(name => name.Substring(skipLength)).ToArray());
return signedFields;
}
}
}