I was recently tasked with building a SOAP Client to consume some services provided by eMedNy, New York State Dept. of Health's electronic Medicaid system. While eMedNy provides a number of web services for providers such as medical and prescription history etc., my project focuses on their subscriber (patient) eligibility service. Once you manage to successfully communicate with the service, the actual exchange consists of submitting an X12-formatted 270 file which is an eligibility request and receiving a 271 (eligibility response) in return.
Working with this arcane format is challenging in its own right and is more than worthy of its own post. However, configuring the certificates and structuring the SOAP security message headers proved to be even more challenging. I still haven't completely gotten my mind around the concept, but apparently the service was written in Java and uses WS-Security to define its security policy. It's possible that building a client in Java is a smooth process, but doing so in .NET proved to be a quite complicated ordeal. While I can't fully explain all the details of the issues at hand, I can, at least, describe the steps I took to get my project up and running. Yaron Naveh was extremely helpful at each step of the way; hat-tip to him.
Officially, Microsofts's `svcutil` utility was supposed to do all the magic for me: just provide the WSDL URL and the appropriate proxy and config files get generated and you're good to go. This was far from the case. Here's what happened when I ran `svcutil`:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
C:\Users\IKleinman\Temp>svcutil https://service01.emedny.org:7602/MHService?wsdl | |
Microsoft (R) Service Model Metadata Tool | |
[Microsoft (R) Windows (R) Communication Foundation, Version 4.0.30319.17929] | |
Copyright (c) Microsoft Corporation. All rights reserved. | |
Attempting to download metadata from 'https://service01.emedny.org:7602/MHService?wsdl' using WS-Metadata Exchange or DISCO. | |
Warning: The following Policy Assertions were not Imported: | |
XPath://wsdl:definitions[@targetNamespace='http://org/emedny/mhs/']/wsdl:binding[@name='MHS'] | |
Assertions: | |
<dpe:summary xmlns:dpe='http://www.datapower.com/extensions'>..</dpe:summary> | |
<sp:SupportingTokens xmlns:sp='http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702'>..</sp:SupportingTokens> | |
Generating files... | |
C:\Users\IKleinman\Temp\MHService.cs | |
C:\Users\IKleinman\Temp\output.config |
The generated `output.config` file contains a similar error:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<system.serviceModel> | |
<bindings> | |
<customBinding> | |
<binding name="MHService_MHSPort"> | |
<!-- WsdlImporter encountered unrecognized policy assertions in ServiceDescription 'http://org/emedny/mhs/': --> | |
<!-- <wsdl:binding name='MHS'> --> | |
<!-- <dpe:summary xmlns:dpe="http://www.datapower.com/extensions">..</dpe:summary> --> | |
<!-- <sp:SupportingTokens xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">..</sp:SupportingTokens> --> | |
<textMessageEncoding messageVersion="Soap11" /> | |
<httpTransport /> | |
</binding> | |
</customBinding> | |
</bindings> | |
<client> | |
<endpoint address="https://12.23.28.113:7602/MHService" binding="customBinding" | |
bindingConfiguration="MHService_MHSPort" contract="MHS" name="MHSPort" /> | |
</client> | |
</system.serviceModel> | |
</configuration> |
Now, I haven't even gotten clarity yet as to what exactly this error message means or what is causing it, but as you can see, due to the error, not much configuration is happening here at all. Thus, I don't include this file in my project.
As an aside, the proxy file, `MHService.cs`, contains a whopping 55K loc. Most of this is not relevant to the services I'm using. (I suspect I only need about 30 of those lines, but I haven't gotten to sifting it yet.)
Let's begin with the steps toward putting together a functional outbound SOAP request message.
Here's a sample request message provided by eMedNy:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mhs="http://org/emedny/mhs/" xmlns:urn="urn:hl7-org:v3"> | |
<soapenv:Header> | |
<wsse:Security soap:mustUnderstand="1" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> | |
<wsse:BinarySecurityToken ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-e00c8062-83d2-4f04-88fc-996218e7bb3d">MIICeDCC....(eMedNY signed user MLS cert).......</wsse:BinarySecurityToken> | |
<wsse:BinarySecurityToken ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-c0cc2cd4-cb77-4fa5-abfa-bd485afd1685">MIIDFj.....( eMedNY MLS web-service end-point public cert)........</wsse:BinarySecurityToken> | |
<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-970e9a80-00cc-4c86-8ec4-3ba16e029a5b"> | |
<wsse:Username>....your_username.....</wsse:Username> | |
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">.....your_plaintext_password....</wsse:Password> | |
<wsse:Nonce>KNyu6MsXCkTg4DDyvwvEiw==</wsse:Nonce> | |
<wsu:Created>2010-09-15T18:00:30Z</wsu:Created> | |
</wsse:UsernameToken> | |
<xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> | |
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" /> | |
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> | |
<wsse:SecurityTokenReference> | |
<wsse:Reference URI="#SecurityToken-c0cc2cd4-cb77-4fa5-abfa-bd485afd1685" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" /> | |
</wsse:SecurityTokenReference> | |
</KeyInfo> | |
<xenc:CipherData> | |
<xenc:CipherValue>gpBAWt91pdwhKva............</xenc:CipherValue> | |
</xenc:CipherData> | |
<xenc:ReferenceList> | |
<xenc:DataReference URI="#Enc-0641b860-b16d-4941-91c0-d60bece67794" /> | |
</xenc:ReferenceList> | |
</xenc:EncryptedKey> | |
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> | |
<SignedInfo> | |
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" /> | |
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> | |
<Reference URI="#Id-f10674fd-b999-47c9-9568-c11fa5e5405b"> | |
<Transforms> | |
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> | |
</Transforms> | |
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> | |
<DigestValue>wRUq.........</DigestValue> | |
</Reference> | |
</SignedInfo> | |
<SignatureValue>tBSsaZi........</SignatureValue> | |
<KeyInfo> | |
<wsse:SecurityTokenReference> | |
<wsse:Reference URI="#SecurityToken-e00c8062-83d2-4f04-88fc-996218e7bb3d" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" /> | |
</wsse:SecurityTokenReference> | |
</KeyInfo> | |
</Signature> | |
</wsse:Security> | |
</soapenv:Header> | |
<soapenv:Body wsu:Id="Id-f10674fd-b999-47c9-9568-c11fa5e5405b" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<xenc:EncryptedData Id="Enc-0641b860-b16d-4941-91c0-d60bece67794" Type="http://www.w3.org/2001/04/xmlenc#Content" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> | |
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /> | |
<xenc:CipherData> | |
<xenc:CipherValue>SQsTCAK6ZaVhojB8+Y.........</xenc:CipherValue> | |
</xenc:CipherData> | |
</xenc:EncryptedData> | |
</soapenv:Body> | |
</soapenv:Envelope> |
While it's true that a message doesn't have to conform entirely to this sample, it gives us something to work towards.Here is an outline of the architecture we can derive from this sample:
So here's a first draft of my code which generates a working request (based on Yaron's gist):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
AsymmetricSecurityBindingElement sec; | |
sec = (AsymmetricSecurityBindingElement) SecurityBindingElement.CreateMutualCertificateBindingElement( | |
MessageSecurityVersion.WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10); | |
sec.DefaultAlgorithmSuite = SecurityAlgorithmSuite.TripleDesRsa15; | |
sec.IncludeTimestamp = false; | |
sec.AllowSerializedSigningTokenOnReply = true; | |
sec.MessageProtectionOrder = MessageProtectionOrder.SignBeforeEncrypt; | |
sec.EndpointSupportingTokenParameters.Signed.Add( new UserNameSecurityTokenParameters( )); | |
CustomBinding binding = new CustomBinding(); | |
binding.Elements.Add(sec); | |
binding.Elements.Add(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8)); | |
binding.Elements.Add(new HttpsTransportBindingElement()); | |
EndpointIdentity ei = new DnsEndpointIdentity( "DPMedsHistory"); | |
EndpointAddress ea = new EndpointAddress( new Uri("https://service01.emedny.org:7602/MHService"), | |
ei, new AddressHeaderCollection()); | |
mhsClient = new MHSClient( binding, ea ); | |
mhsClient.ClientCredentials.ServiceCertificate.DefaultCertificate = | |
new X509Certificate2( // make the server certificate availble here ); | |
mhsClient.ClientCredentials.ClientCertificate.Certificate = | |
new X509Certificate2( // make your client certificate available here ); | |
mhsClient.ClientCredentials.UserName.UserName = "username"; | |
// this is a password you establish with eMedNy especially for this purpose | |
mhsClient.ClientCredentials.UserName.Password = "password"; | |
mhsClient.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = | |
System.ServiceModel.Security.X509CertificateValidationMode.None; | |
Transaction request = new Transaction(); | |
request.transData = Encoding.UTF8.GetBytes( "270 message" ); | |
Transaction response; | |
response = mhsClient.getEligibility( request ); | |
string _271Message = Encoding.UTF8.GetString( response.transData ); |
And here what the generated message looks like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<s:Header> | |
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> | |
<o:BinarySecurityToken u:Id="uuid-604c12aa-1a76-4f40-b7e3-57857b596549-3" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"> | |
MIICL...(USER CERTIFICATE)...lmdGpDz8gbg646R5gzebF1waazrrVM=</o:BinarySecurityToken> | |
<e:EncryptedKey Id="_0" xmlns:e="http://www.w3.org/2001/04/xmlenc#"> | |
<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/> | |
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> | |
<o:SecurityTokenReference> | |
<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"> | |
WwRKuiXJ0r+vSeYbygGXT6bVovc=</o:KeyIdentifier> | |
</o:SecurityTokenReference> | |
</KeyInfo> | |
<e:CipherData> | |
<e:CipherValue> | |
SnyKmgr7+Ob...(CIPHER VALUE)...sGfQClt4lbQ=</e:CipherValue> | |
</e:CipherData> | |
<e:ReferenceList> | |
<e:DataReference URI="#_2"/> | |
</e:ReferenceList> | |
</e:EncryptedKey> | |
<o:UsernameToken u:Id="uuid-604c12aa-1a76-4f40-b7e3-57857b596549-1"> | |
<o:Username> | |
USERNAME</o:Username> | |
<o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"> | |
PASSWORD</o:Password> | |
</o:UsernameToken> | |
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> | |
<SignedInfo> | |
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> | |
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> | |
<Reference URI="#_1"> | |
<Transforms> | |
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> | |
</Transforms> | |
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> | |
<DigestValue> | |
LF0W3vbNDhuRGWjoWATWCXzfg4=</DigestValue> | |
</Reference> | |
<Reference URI="#uuid-604c12aa-1a76-4f40-b7e3-57857b596549-1"> | |
<Transforms> | |
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> | |
</Transforms> | |
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> | |
<DigestValue> | |
cqukn9h6mWxsdb/3Ukrok1GlKJk=</DigestValue> | |
</Reference> | |
</SignedInfo> | |
<SignatureValue> | |
gKtBufj7n4JyF93S/75KBq4gtmP0m/ulP9zfDRjBCk/TbpwqAoucHhR/X2a+z9Vdfh7W5wdVyh+7Q==</SignatureValue> | |
<KeyInfo> | |
<o:SecurityTokenReference> | |
<o:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" URI="#uuid-604c12aa-1a76-4f40-b7e3-57857b596549-3"/> | |
</o:SecurityTokenReference> | |
</KeyInfo> | |
</Signature> | |
</o:Security> | |
</s:Header> | |
<s:Body u:Id="_1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> | |
<e:EncryptedData Id="_2" Type="http://www.w3.org/2001/04/xmlenc#Content" xmlns:e="http://www.w3.org/2001/04/xmlenc#"> | |
<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/> | |
<e:CipherData> | |
<e:CipherValue> | |
Bn7xoRMGelV06MS...(CIPHER VALUE)....l89KHppeFhyl8ETu9l4VMhAb4=</e:CipherValue> | |
</e:CipherData> | |
</e:EncryptedData> | |
</s:Body> | |
</s:Envelope> |
As you can see, the message I generate differs slightly from the sample they provide.
In particular, my version does not have:
1. the `nonce` and `timestamp`
2. the server certificate ( it only gets referenced by `Subject Key Identifier`
but.. it works.
Now, let's proceed to dealing with the response message.
If you try running your program using the above code, you'll get the following error:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Unhandled Exception: System.ServiceModel.Security.SecurityNegotiationException: Could not establish | |
trust relationship for the SSL/TLS secure channel with authority 'service01.emedny.org:7602'. | |
---> System.Net.WebException: The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel. ---> |
The issue here is that eMedNy's server is presenting a certificate which is not valid according to the client's trust chain. Yaron addresses this issue here. You incorporate his code as follows:
1. add the following callback to the program:
static bool OnValidationCallback( object sender, X509Certificate cert,
X509Chain chain, SslPolicyErrors errors)
{
return true;
}
X509Chain chain, SslPolicyErrors errors)
{
return true;
}
2. and, in your configuration code, perform the following assignment:
ServicePointManager.ServerCertificateValidationCallback = new
RemoteCertificateValidationCallback(OnValidationCallback);
After making that change, running the program should give you this error:RemoteCertificateValidationCallback(OnValidationCallback);
Unhandled Exception: System.ServiceModel.Security.MessageSecurityException: The incoming message was signed with a token which was different from what used to encrypt the body. This was not expected.
For some reason, WCF is not properly identifying the server certificate token. Hard as I tried, I have not (yet) been able to figure out how to tweak the configuration to overcome this issue. As a last resort, I had no choice but to roll my own custom encoder. The code is based on the examples provided on MSDN, but I've tried to remove a lot of the parts that are not needed for our case, so that it's somewhat more obvious what the code does.
So here is the CustomTextMessageBindingElement class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.ServiceModel.Channels; | |
using System.Xml; | |
namespace eMedNySOAPClient | |
{ | |
public class CustomTextMessageBindingElement : MessageEncodingBindingElement | |
{ | |
private MessageVersion msgVersion; | |
private string mediaType; | |
private string encoding; | |
private XmlDictionaryReaderQuotas readerQuotas; | |
public CustomTextMessageBindingElement( ) | |
{ | |
MessageVersion = MessageVersion.Soap11; | |
mediaType = "text/xml"; | |
readerQuotas = new XmlDictionaryReaderQuotas( ); | |
} | |
public override MessageVersion MessageVersion | |
{ | |
get | |
{ | |
return this.msgVersion; | |
} | |
set | |
{ | |
if (value == null) | |
throw new ArgumentNullException("value"); | |
this.msgVersion = value; | |
} | |
} | |
public string MediaType | |
{ | |
get | |
{ | |
return this.mediaType; | |
} | |
set | |
{ | |
if (value == null) | |
throw new ArgumentNullException("value"); | |
this.mediaType = value; | |
} | |
} | |
public string Encoding | |
{ | |
get | |
{ | |
return this.encoding; | |
} | |
set | |
{ | |
if (value == null) | |
throw new ArgumentNullException("value"); | |
this.encoding = value; | |
} | |
} | |
// This encoder does not enforces any quotas for the unsecure messages. The | |
// quotas are enforced for the secure portions of messages when this encoder | |
// is used in a binding that is configured with security. | |
public XmlDictionaryReaderQuotas ReaderQuotas | |
{ | |
get | |
{ | |
return this.readerQuotas; | |
} | |
} | |
#region IMessageEncodingBindingElement Members | |
public override MessageEncoderFactory CreateMessageEncoderFactory() | |
{ | |
return new CustomTextMessageEncoderFactory( MediaType, MessageVersion); | |
} | |
#endregion | |
public override BindingElement Clone() | |
{ | |
return new CustomTextMessageBindingElement( ); | |
} | |
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context) | |
{ | |
if (context == null) | |
throw new ArgumentNullException("context"); | |
context.BindingParameters.Add(this); | |
return context.BuildInnerChannelFactory<TChannel>(); | |
} | |
public override bool CanBuildChannelFactory<TChannel>(BindingContext context) | |
{ | |
if (context == null) | |
throw new ArgumentNullException("context"); | |
return context.CanBuildInnerChannelFactory<TChannel>(); | |
} | |
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context) | |
{ | |
if (context == null) | |
throw new ArgumentNullException("context"); | |
context.BindingParameters.Add(this); | |
return context.BuildInnerChannelListener<TChannel>(); | |
} | |
public override bool CanBuildChannelListener<TChannel>(BindingContext context) | |
{ | |
if (context == null) | |
throw new ArgumentNullException("context"); | |
context.BindingParameters.Add(this); | |
return context.CanBuildInnerChannelListener<TChannel>(); | |
} | |
public override T GetProperty<T>(BindingContext context) | |
{ | |
if (typeof(T) == typeof(XmlDictionaryReaderQuotas)) | |
{ | |
return (T)(object)this.readerQuotas; | |
} | |
else | |
{ | |
return base.GetProperty<T>(context); | |
} | |
} | |
} | |
} |
here is the CustomTextMessageEncoderFactory class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.ServiceModel.Channels; | |
namespace eMedNySOAPClient | |
{ | |
public class CustomTextMessageEncoderFactory : MessageEncoderFactory | |
{ | |
private MessageEncoder encoder; | |
internal CustomTextMessageEncoderFactory(string contentType, MessageVersion msgVersion) | |
{ | |
this.encoder = new CustomTextMessageEncoder(contentType, msgVersion); | |
} | |
public override MessageEncoder Encoder | |
{ | |
get | |
{ | |
return this.encoder; | |
} | |
} | |
public override MessageVersion MessageVersion | |
{ | |
get | |
{ | |
return this.encoder.MessageVersion; | |
} | |
} | |
} | |
} |
and, finally, here is the actual CustomTextMessageEncoder class. The `ReadMessage` method is where the decryption takes place. I intend to make the method a bit neater using the `EncryptedXml` class, but here it is for now:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text; | |
using System.ServiceModel.Channels; | |
using System.Xml; | |
using System.IO; | |
using System.Security.Cryptography; | |
using System.Resources; | |
using System.Security.Cryptography.X509Certificates; | |
namespace eMedNySOAPClient | |
{ | |
public class CustomTextMessageEncoder : MessageEncoder | |
{ | |
private string contentType; | |
private MessageVersion msgVersion; | |
public CustomTextMessageEncoder(string contentType, MessageVersion msgVersion) | |
{ | |
this.contentType = contentType; | |
this.msgVersion = msgVersion; | |
} | |
public override string ContentType | |
{ | |
get | |
{ | |
return contentType; | |
} | |
} | |
public override string MediaType | |
{ | |
get | |
{ | |
return string.Empty; | |
} | |
} | |
public override MessageVersion MessageVersion | |
{ | |
get | |
{ | |
return msgVersion; | |
} | |
} | |
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType) | |
{ | |
byte[] msgContents = new byte[buffer.Count]; | |
Array.Copy(buffer.Array, buffer.Offset, msgContents, 0, msgContents.Length); | |
bufferManager.ReturnBuffer(buffer.Array); | |
MemoryStream stream = new MemoryStream(msgContents); | |
var doc = new XmlDocument(); | |
doc.PreserveWhitespace = true; | |
doc.Load(stream); | |
var encryptedKey = doc.GetElementsByTagName( "xenc:EncryptedKey" )[0].InnerText; | |
var cipherText = doc.GetElementsByTagName( "xenc:CipherValue" )[1].InnerText; | |
var cipher = Convert.FromBase64String( cipherText ); | |
var clientCert = new X509Certificate2( //PULL IN YOUR CLIENT CERTIFICATE HERE ); | |
var rsa = (RSACryptoServiceProvider)clientCert.PrivateKey; | |
byte [] key = rsa.Decrypt(Convert.FromBase64String(encryptedKey), false); | |
SymmetricAlgorithm alg = new TripleDESCryptoServiceProvider(); | |
alg.KeySize = 192; | |
alg.Mode = CipherMode.CBC; | |
alg.Padding = PaddingMode.ISO10126; | |
byte[] IV = new byte[8]; | |
Array.Copy(cipher, IV, 8); | |
MemoryStream ms = new MemoryStream( cipher , IV.Length, cipher.Length-IV.Length); | |
CryptoStream csDecrypt = new CryptoStream( ms, alg.CreateDecryptor(key, IV ), CryptoStreamMode.Read); | |
byte [] fromEnc = new byte[cipher.Length]; | |
csDecrypt.Read(fromEnc, 0, fromEnc.Length); | |
string messageBody = UTF8Encoding.UTF8.GetString(fromEnc); | |
XmlDocumentFragment bodyElement = doc.CreateDocumentFragment(); | |
bodyElement.InnerXml = messageBody; | |
var body = (XmlNode)doc.GetElementsByTagName("soapenv:Body")[0]; | |
body.ReplaceChild( bodyElement, body.FirstChild ); | |
doc.DocumentElement.RemoveChild( doc.GetElementsByTagName( "soapenv:Header" )[0] ); | |
stream = new MemoryStream(); | |
doc.Save(stream); | |
stream.Position = 0; | |
return Message.CreateMessage(XmlReader.Create(stream), int.MaxValue, this.MessageVersion); | |
} | |
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType) | |
{ | |
XmlReader reader = XmlReader.Create(stream); | |
var message = Message.CreateMessage(reader, maxSizeOfHeaders, this.MessageVersion); | |
return message; | |
} | |
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) | |
{ | |
MemoryStream stream = new MemoryStream(); | |
XmlWriter writer = XmlWriter.Create(stream); | |
message.WriteMessage(writer); | |
writer.Close(); | |
byte[] messageBytes = stream.GetBuffer(); | |
int messageLength = (int)stream.Position; | |
int totalLength = messageLength + messageOffset; | |
byte[] totalBytes = bufferManager.TakeBuffer(totalLength); | |
Array.Copy(messageBytes, 0, totalBytes, messageOffset, messageLength); | |
ArraySegment<byte> byteArray = new ArraySegment<byte>(totalBytes, messageOffset, messageLength); | |
return byteArray; | |
} | |
public override void WriteMessage(Message message, Stream stream) | |
{ | |
XmlWriter writer = XmlWriter.Create(stream); | |
message.WriteMessage(writer); | |
writer.Close(); | |
} | |
} | |
} |
At this point, we can go back to the configuration code and replace the use of the default encoder:
binding.Elements.Add(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8));
with our custom encoder as follows:
binding.Elements.Add(new CustomTextMessageBindingElement());
As a relative n00b, getting this to work was quite challenging, so I hope this can help some others in the same situation. What's next? get this blog rss updates or register for mail updates!