WCF Security Interoperability Guidelines – 1 : Reference Style of a Primary Signing Token inside a response

Making things work with a disparate entity is always a difficult task to accomplish. Things are no different in WCF Interoperability space. While significant developments have been made over the years to standardize various protocols and develop products which adhere to these protocols, differences are always there. Through the course of this article and subsequent ones, I will present certain painful interoperability issues which we have encountered in WCF support life cycle so far. Note that these are simple guidelines of what works and what does not as it stands today. Workarounds will be presented for some, while the rest are by design.

 

Such issues are always manifested by either communicating parties’ inability to process a request / response generated by their counterpart. It’s always either ‘a WCF service not able to process a request generated by a non - WCF client’ or ‘a WCF client notable to process a response received from a non – WCF web service’. My intention is to pick a couple such scenarios and perform a post – mortem.

 

Scenario: Reference Style of a Primary Signing Token inside a response

Consider a WCF client consuming a non – .NET web service. Following are the security attributes of the underlying communication:

                -     Security is implemented at the message layer with either parties communicating over HTTP.

                -     Both client and service mutually authenticates via X509 certificates.

                -     Additional client authentication in terms of a supporting username token.

 

One of the key requirements is to understand how any given security scenario can be represented in WCF world. Given the above points, we need to implement a custom binding (no standard binding can meet all the above requirements) which looks as follows:

 

HttpTransportBindingElement http = new HttpTransportBindingElement();

 

X509SecurityTokenParameters initiatorToken = new X509SecurityTokenParameters();

initiatorToken.RequireDerivedKeys = false;

           

X509SecurityTokenParameters receipientToken = new X509SecurityTokenParameters();

receipientToken.RequireDerivedKeys = false;

           

AsymmetricSecurityBindingElement asbe = new AsymmetricSecurityBindingElement(receipientToken,initiatorToken);

asbe.IncludeTimestamp = true;

asbe.RequireSignatureConfirmation = false;

           

UserNameSecurityTokenParameters username = new UserNameSecurityTokenParameters();

username.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;

username.RequireDerivedKeys = false;

 

asbe.EndpointSupportingTokenParameters.Signed.Add(username);

 

TextMessageEncodingBindingElement tmbe = new TextMessageEncodingBindingElement(MessageVersion.Soap11,System.Text.Encoding.UTF8);

 

return new CustomBinding(tmbe, asbe, http);

 

Let’s now get to the issue. With this binding in place, a call to a non - .NET web service fails with the following exception:

 

<Exception>

          <ExceptionType>System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>

          <Message>There was an error deserializing the security key identifier clause XML. Please see the inner exception for more details.</Message>

<StackTrace>

         at System.ServiceModel.Security.WSSecurityTokenSerializer.ReadKeyIdentifierClauseCore(XmlReader reader)

         at System.IdentityModel.Selectors.SecurityTokenSerializer.ReadKeyIdentifierClause(XmlReader reader)

         at System.ServiceModel.Security.XmlDsigSep2000.KeyInfoEntry.ReadKeyIdentifierCore(XmlDictionaryReader reader)

         at System.ServiceModel.Security.WSSecurityTokenSerializer.ReadKeyIdentifierCore(XmlReader reader)

         at System.IdentityModel.Selectors.SecurityTokenSerializer.ReadKeyIdentifier(XmlReader reader)

         at System.IdentityModel.Signature.ReadFrom(XmlDictionaryReader reader, DictionaryManager dictionaryManager)

         at System.IdentityModel.SignedXml.ReadFrom(XmlDictionaryReader reader)

         at System.ServiceModel.Security.WSSecurityOneDotZeroReceiveSecurityHeader.ReadSignatureCore(XmlDictionaryReader signatureReader)

         at System.ServiceModel.Security.ReceiveSecurityHeader.ReadSignature(XmlDictionaryReader reader, Int32 position, Byte[] decryptedBuffer)

         at System.ServiceModel.Security.ReceiveSecurityHeader.ExecuteFullPass(XmlDictionaryReader reader)

         at System.ServiceModel.Security.StrictModeSecurityHeaderElementInferenceEngine.ExecuteProcessingPasses(ReceiveSecurityHeader securityHeader, XmlDictionaryReader reader)

         at System.ServiceModel.Security.ReceiveSecurityHeader.Process(TimeSpan timeout)

         .

         .

         .

 

A look at the above stack trace shows that processing of response security header throws an exception. How do we know that? Note the highlighted last line of the exception stack trace. Following is the inner exception:

 

<InnerException>

         <ExceptionType>System.ArgumentNullException, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>

         <Message>Value cannot be null.

         Parameter name: certificate</Message>

<StackTrace>

         at System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause.GetRawData(X509Certificate certificate)

         at System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause..ctor(X509Certificate2 certificate)

         at System.ServiceModel.Security.XmlDsigSep2000.X509CertificateClauseEntry.ReadKeyIdentifierClauseCore(XmlDictionaryReader reader)

         at System.ServiceModel.Security.WSSecurityTokenSerializer.ReadKeyIdentifierClauseCore(XmlReader reader)

</StackTrace>

</InnerException>

 

So we are failing to retrieve key identifiers of primary signing token from the received signature. Let’s have a look at the received primary signing token:

 

<ds:KeyInfo>

    <ds:X509Data>

        <ds:X509IssuerSerial>

            <ds:X509IssuerName>XXXXXXXXXXXXXXXXXXX </ds:X509IssuerName>

            <ds:X509SerialNumber>

                       74990459506171493075258200981073178724

            </ds:X509SerialNumber>

        </ds:X509IssuerSerial>

    </ds:X509Data>

</ds:KeyInfo>

 

So what’s wrong with this? Why are we trying to construct a System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause (based on the stack trace shared above) when the service has passed an IssuerSerial of the signing certificate? Let me explain a bit of what happens at the background. Take a look at the reflected code of System.ServiceModel.Security.WSSecurityTokenSerializer.ReadKeyIdentifierClauseCore:

 

      protected override SecurityKeyIdentifierClause ReadKeyIdentifierClauseCore(XmlReader reader)

      {

              XmlDictionaryReader reader2 = XmlDictionaryReader.CreateDictionaryReader(reader);

              for (int i = 0; i < this.keyIdentifierClauseEntries.Count; i++)

              {

               KeyIdentifierClauseEntry entry = this.keyIdentifierClauseEntries[i];

                      if (entry.CanReadKeyIdentifierClauseCore(reader2))

                      {

                         try

                         {

                              return entry.ReadKeyIdentifierClauseCore(reader2);

                         }

 

Couple of points to note at this point:

             1. System.ServiceModel.Security.WSSecurityTokenSerializer+KeyIdentifierClauseEntry: It represents core identifying properties of an X509Token. This is derived by a number of classes, one beingSystem.ServiceModel.Security.XmlDsigSep2000.X509CertificateClauseEntry. This class is of interest to us since it appears in the failing inner exception stack trace.

           2. KeyIdentifierClauseEntry.CanReadKeyIdentifierClauseCore method: this method determines which KeyIdentifierClauseEntry class is used to retrieve key identifier. Reflected code of the method:

 

      public virtual bool CanReadKeyIdentifierClauseCore(XmlDictionaryReader reader)

      {

          return reader.IsStartElement(this.LocalName, this.NamespaceUri);

      }

 

Consider the iteration when the value of entry variable in the above ReadKeyIdentifierClauseCore method is set to X509CertificateClauseEntry. At the same time:

 

                 a. reader is pointing to <ds:X509Data> element

                 b. this.LocalName inside CanReadKeyIdentifierClauseCore is set to X509Data

 

CanReadKeyIdentifierClauseCore returns true and control gets into X509CertificateClauseEntry.ReadKeyIdentifierClauseCore. Inside this method, we try to generate X509RawDataKeyIdentifierClause which fails. From this we can conclude that CanReadKeyIdentifierClauseCore method should not return TRUE in the first place. While the value of X509CertificateClauseEntry.LocalName is predefined (X509Data), it means that there is something wrong with the structure of primary token. There are 2 possibilities here:

 

                a. Either <ds:X509Data> should not be the immediate child element of <ds:KeyInfo>.

                b. If <ds:X509Data> is the immediate child of <ds:KeyInfo>, the key identifier clauses should be different.

  

So how a primary signing token should be referenced? WCF supports 4 ways of referencing a primary X509 token. This is defined by System.ServiceModel.Security.Tokens.X509KeyIdentifierClauseType which is an enumeration type. Following are its possible values (SubjectKeyIdentifier is based on subject key identifier extension and is not explained below):

 

 

        1. Any / IssuerSerial (default)

 

        <KeyInfo>

          <o:SecurityTokenReference>

                  <X509Data>

                     <X509IssuerSerial>

                           <X509IssuerName>CN=XXXXX</X509IssuerName>

                           <X509SerialNumber>

                                     -128364659718044525824792132

                          </X509SerialNumber>

                     </X509IssuerSerial>

                 </X509Data>

         </o:SecurityTokenReference>

        </KeyInfo>

 

        2. RawDataKeyIdentifier

 

        <KeyInfo>

         <X509Data>

                 <X509Certificate>MIICAjCCAWugAwIBAgIQwZyW5YOCXZxHg1MBV2CpvDANBgkhkiG9w0BAQnEdD9tI7IYAAoK4O+35EOzcXbvc4Kzz7BQnulQ= </X509Certificate>

         </X509Data>

        </KeyInfo>

 

        3. Thumbprint

 

        <KeyInfo>

          <o:SecurityTokenReference>

                       <o:KeyIdentifier ValueType="https://docs.oasis-open.org/wss/oasis-wss-soap-message-security1.1#ThumbprintSHA1">9JscCwWHk5IvR/6JLTSayTY7M= </o:KeyIdentifier>

          </o:SecurityTokenReference>

        </KeyInfo>

 

 

Hence to answer (a) and (b):-

 

                a. When passing <X509IssuerSerial>, <X509Data> element should be child to a <o:SecurityTokenReference> element, not <KeyInfo> element.

                b. When <X509Data> is passed as immediate child element to <KeyInfo>, its child element should be <X509Certificate>. This proves why we see X509CertificateClauseEntry in the exception stack trace.

 

Now that we know why the response failed at the client end, let’s talk about a workaround. A simple workaround will be to modify the incoming response using a custom channel. I will not get into the intricacies of how to write a custom channel in this blog article. That is available under ‘channel extensibility samples’ here. Going by the primary signing token shared earlier:

 

        <ds:KeyInfo>

             <o:SecurityTokenReference> ----------------------- INSERT THIS ELEMENT

                         <ds:X509Data>

                                <ds:X509IssuerSerial>

                                           <ds:X509IssuerName>XXXXXXXXXXXXXXXXXXX </ds:X509IssuerName>

                                           <ds:X509SerialNumber>

                                                     74990459506171493075258200981073178724

                                           </ds:X509SerialNumber>

                               </ds:X509IssuerSerial>

                         </ds:X509Data>

            </o:SecurityTokenReference>

        </ds:KeyInfo>

 

Following are the steps achieve that (assuming that the message is only signed):

 

A.Operate on a message object to insert necessary element as highlighted above

 

private Message OnReceiveMessage(Message response)

{

 

     MemoryStream responseStream = new MemoryStream();

     MessageBuffer responseBuffer = response.CreateBufferedCopy(Int32.MaxValue);

     responseBuffer.WriteMessage(responseStream);

     responseStream.Position = 0;

     byte[] respArr = responseStream.ToArray();

 

     string responseMessage = System.Text.Encoding.ASCII.GetString(respArr);

 

     XmlDocument resDom = new XmlDocument();

     resDom.LoadXml(responseMessage);

 

     /*

     * Fetch <X509Data> element from the received response message

     */

     XmlNodeList x509DataElement = resDom.GetElementsByTagName("X509Data");

     bool modificationRequired = true;

 

     if (x509DataElement[0].FirstChild.Name.Equals("X509Certificate"))

     {

   return response;

     }

     else

     {

   modificationRequired = true;

     }

 

     if (modificationRequired)

     {

   XmlElement securityTokenReference = resDom.CreateElement("o", "SecurityTokenReference", WSSecurityURI);

   securityTokenReference.AppendChild(x509DataElement[0]);

 

   XmlNodeList keyInfoElement = resDom.GetElementsByTagName("KeyInfo");

   keyInfoElement[0].AppendChild(securityTokenReference);

     }

           

     string modifiedResponseMessageStr = resDom.InnerXml;

 

     byte[] modifedResponseMessageInBytes = System.Text.Encoding.UTF8.GetBytes(modifiedResponseMessageStr);

 

     XmlDictionaryReaderQuotas quotas = new XmlDictionaryReaderQuotas();

 

     XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(modifedResponseMessageInBytes, quotas);

 

     Message newResponseMessage = Message.CreateMessage(reader, 2147483647, response.Version);

 

     MessageBuffer modifiedResponseBuffer = newResponseMessage.CreateBufferedCopy(Int32.MaxValue);

 

     return modifiedResponseBuffer.CreateMessage();  

}

 

B. Call ReceiveMessage from IRequestChannel.Request method implementation

 

public Message Request(Message message)

{

     Message retmsg = m_InnerRequestChannel.Request(message);

           

     MessageBuffer responseBufferCopy = retmsg.CreateBufferedCopy(Int16.MaxValue);

 

     return OnReceiveMessage(responseBufferCopy.CreateMessage());

}

 

With that we know what X509ReferenceStyles are supported by WCF and how to work around any mismatch arising out of it. Next up we will see interoperability issues arising from <o:BinarySecurityToken> present within a request security header.