summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
committerAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
commit5fec515095ee10b522f414a03e78f282aaf520dc (patch)
tree204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements
parentf1a4155398635a4fd9f485eec817152627682704 (diff)
parent8f4165ee515728aca3faaa26e8354a40612e85e4 (diff)
downloadDotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.zip
DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.gz
DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.bz2
Merge branch 'splitDlls'.
DNOA now builds and (in some cases) ships as many distinct assemblies.
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements')
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/AssociateUnencryptedProviderRequest.cs41
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderChannel.cs76
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderMessageFactory.cs86
-rw-r--r--src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/ProviderSigningBindingElement.cs251
4 files changed, 454 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/AssociateUnencryptedProviderRequest.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/AssociateUnencryptedProviderRequest.cs
new file mode 100644
index 0000000..322c435
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/AssociateUnencryptedProviderRequest.cs
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociateUnencryptedProviderRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Represents an association request received by the OpenID Provider that is sent using HTTPS and
+ /// otherwise communicates the shared secret in plain text.
+ /// </summary>
+ internal class AssociateUnencryptedProviderRequest : AssociateUnencryptedRequest, IAssociateRequestProvider {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssociateUnencryptedProviderRequest"/> class.
+ /// </summary>
+ /// <param name="version">The OpenID version this message must comply with.</param>
+ /// <param name="providerEndpoint">The OpenID Provider endpoint.</param>
+ internal AssociateUnencryptedProviderRequest(Version version, Uri providerEndpoint)
+ : base(version, providerEndpoint) {
+ }
+
+ /// <summary>
+ /// Creates a Provider's response to an incoming association request.
+ /// </summary>
+ /// <returns>
+ /// The appropriate association response message.
+ /// </returns>
+ public IProtocolMessage CreateResponseCore() {
+ var response = new AssociateUnencryptedResponseProvider(this.Version, this);
+ response.AssociationType = this.AssociationType;
+ return response;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderChannel.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderChannel.cs
new file mode 100644
index 0000000..5812a96
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderChannel.cs
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdProviderChannel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Provider;
+
+ /// <summary>
+ /// The messaging channel for OpenID Providers.
+ /// </summary>
+ internal class OpenIdProviderChannel : OpenIdChannel {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdProviderChannel"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The OpenID Provider's association store or handle encoder.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal OpenIdProviderChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings)
+ : this(cryptoKeyStore, nonceStore, new OpenIdProviderMessageFactory(), securitySettings) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNull(securitySettings, "securitySettings");
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdProviderChannel"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The association store to use.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="messageTypeProvider">An object that knows how to distinguish the various OpenID message types for deserialization purposes.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ private OpenIdProviderChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, ProviderSecuritySettings securitySettings)
+ : base(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings)) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNull(messageTypeProvider, "messageTypeProvider");
+ Requires.NotNull(securitySettings, "securitySettings");
+ }
+
+ /// <summary>
+ /// Initializes the binding elements.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The OpenID Provider's crypto key store.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="securitySettings">The security settings to apply. Must be an instance of either RelyingPartySecuritySettings or ProviderSecuritySettings.</param>
+ /// <returns>
+ /// An array of binding elements which may be used to construct the channel.
+ /// </returns>
+ private static IChannelBindingElement[] InitializeBindingElements(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNull(securitySettings, "securitySettings");
+ Requires.NotNull(nonceStore, "nonceStore");
+
+ SigningBindingElement signingElement;
+ signingElement = new ProviderSigningBindingElement(cryptoKeyStore, securitySettings);
+
+ var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration();
+
+ List<IChannelBindingElement> elements = new List<IChannelBindingElement>(8);
+ elements.Add(new ExtensionsBindingElement(extensionFactory, securitySettings, true));
+ elements.Add(new StandardReplayProtectionBindingElement(nonceStore, true));
+ elements.Add(new StandardExpirationBindingElement());
+ elements.Add(signingElement);
+
+ return elements.ToArray();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderMessageFactory.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderMessageFactory.cs
new file mode 100644
index 0000000..3fab06b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/OpenIdProviderMessageFactory.cs
@@ -0,0 +1,86 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdProviderMessageFactory.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// OpenID Provider message factory.
+ /// </summary>
+ internal class OpenIdProviderMessageFactory : IMessageFactory {
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="recipient">The intended or actual recipient of the request message.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ public IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) {
+ RequestBase message = null;
+
+ // Discern the OpenID version of the message.
+ Protocol protocol = Protocol.V11;
+ string ns;
+ if (fields.TryGetValue(Protocol.V20.openid.ns, out ns)) {
+ ErrorUtilities.VerifyProtocol(string.Equals(ns, Protocol.OpenId2Namespace, StringComparison.Ordinal), MessagingStrings.UnexpectedMessagePartValue, Protocol.V20.openid.ns, ns);
+ protocol = Protocol.V20;
+ }
+
+ string mode;
+ if (fields.TryGetValue(protocol.openid.mode, out mode)) {
+ if (string.Equals(mode, protocol.Args.Mode.associate)) {
+ if (fields.ContainsKey(protocol.openid.dh_consumer_public)) {
+ message = new AssociateDiffieHellmanProviderRequest(protocol.Version, recipient.Location);
+ } else {
+ message = new AssociateUnencryptedProviderRequest(protocol.Version, recipient.Location);
+ }
+ } else if (string.Equals(mode, protocol.Args.Mode.checkid_setup) ||
+ string.Equals(mode, protocol.Args.Mode.checkid_immediate)) {
+ AuthenticationRequestMode authMode = string.Equals(mode, protocol.Args.Mode.checkid_immediate) ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup;
+ if (fields.ContainsKey(protocol.openid.identity)) {
+ message = new CheckIdRequest(protocol.Version, recipient.Location, authMode);
+ } else {
+ ErrorUtilities.VerifyProtocol(!fields.ContainsKey(protocol.openid.claimed_id), OpenIdStrings.IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent);
+ message = new SignedResponseRequest(protocol.Version, recipient.Location, authMode);
+ }
+ } else if (string.Equals(mode, protocol.Args.Mode.check_authentication)) {
+ message = new CheckAuthenticationRequest(protocol.Version, recipient.Location);
+ } else {
+ ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessagePartValue, protocol.openid.mode, mode);
+ }
+ }
+
+ if (message != null) {
+ message.SetAsIncoming();
+ }
+
+ return message;
+ }
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="request">The message that was sent as a request that resulted in the response.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ public IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) {
+ // OpenID Providers make no outbound requests, and thus receive no direct response messages.
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/ProviderSigningBindingElement.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/ProviderSigningBindingElement.cs
new file mode 100644
index 0000000..e257e63
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/ChannelElements/ProviderSigningBindingElement.cs
@@ -0,0 +1,251 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProviderSigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.Provider;
+
+ /// <summary>
+ /// The signing binding element for OpenID Providers.
+ /// </summary>
+ internal class ProviderSigningBindingElement : SigningBindingElement {
+ /// <summary>
+ /// The association store used by Providers to look up the secrets needed for signing.
+ /// </summary>
+ private readonly IProviderAssociationStore opAssociations;
+
+ /// <summary>
+ /// The security settings at the Provider.
+ /// Only defined when this element is instantiated to service a Provider.
+ /// </summary>
+ private readonly ProviderSecuritySettings opSecuritySettings;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderSigningBindingElement"/> class.
+ /// </summary>
+ /// <param name="associationStore">The association store used to look up the secrets needed for signing.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal ProviderSigningBindingElement(IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) {
+ Requires.NotNull(associationStore, "associationStore");
+ Requires.NotNull(securitySettings, "securitySettings");
+
+ this.opAssociations = associationStore;
+ this.opSecuritySettings = securitySettings;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this binding element is on a Provider channel.
+ /// </summary>
+ protected override bool IsOnProvider {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// 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.
+ /// </returns>
+ public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ var result = base.ProcessOutgoingMessage(message);
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the association to use to sign or verify a message.
+ /// </summary>
+ /// <param name="signedMessage">The message to sign or verify.</param>
+ /// <returns>
+ /// The association to use to sign or verify the message.
+ /// </returns>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Gets a specific association referenced in a given message's association handle.
+ /// </summary>
+ /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param>
+ /// <returns>
+ /// The referenced association; or <c>null</c> if such an association cannot be found.
+ /// </returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets a private Provider association used for signing messages in "dumb" mode.
+ /// </summary>
+ /// <returns>An existing or newly created association.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Verifies the signature by unrecognized handle.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="signedMessage">The signed message.</param>
+ /// <param name="protectionsApplied">The protections applied.</param>
+ /// <returns>
+ /// The applied protections.
+ /// </returns>
+ protected override MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied) {
+ // 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).
+ throw new InvalidSignatureException(message);
+ }
+
+ /// <summary>
+ /// Determines whether the relying party sending an authentication request is
+ /// vulnerable to replay attacks.
+ /// </summary>
+ /// <param name="request">The request message from the Relying Party. Useful, but may be null for conservative estimate results.</param>
+ /// <param name="response">The response message to be signed.</param>
+ /// <returns>
+ /// <c>true</c> if the relying party is vulnerable; otherwise, <c>false</c>.
+ /// </returns>
+ 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[ReturnToNonceBindingElement.NonceParameter])) {
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the value to use for the openid.signed parameter.
+ /// </summary>
+ /// <param name="signedMessage">The signable message.</param>
+ /// <returns>
+ /// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines
+ /// the inclusion and order of message parts that will be signed.
+ /// </returns>
+ private string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) {
+ Requires.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<string> extraSignedParameters = new List<string>(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;
+ }
+ }
+}